iranzo/stampython

View on GitHub
stampy/plugin/stats.py

Summary

Maintainability
F
6 days
Test Coverage
#!/usr/bin/env python
# encoding: utf-8
#
# Description: Plugin for processing stats commands
# Author: Pablo Iranzo Gomez (Pablo.Iranzo@gmail.com)

import datetime
import json
import logging
import urllib

from prettytable import from_db_cursor

import stampy.plugin.config
import stampy.plugin.karma
import stampy.stampy
from stampy.i18n import _
from stampy.i18n import _L


def init():
    """
    Initializes module
    :return: List of triggers for plugin
    """

    triggers = ["@all", "^/stats", "*", "^/getout"]

    stampy.stampy.cronme(name="stats", interval=24 * 60)

    return triggers


def cron():
    """
    Function to be executed periodically
    :return:
    """

    dochatcleanup()
    dousercleanup()


def run(message):  # do not edit this line
    """
    Executes plugin
    :param message: message to run against
    :return:
    """

    logger = logging.getLogger(__name__)

    msgdetail = stampy.stampy.getmsgdetail(message)
    text = msgdetail["text"]

    # Update stats on the message being processed unless we use sudo
    if not stampy.plugin.config.config(key='overridegid', default=False):
        if msgdetail["chat_id"] and msgdetail["chat_name"]:
            updatestats(type=msgdetail['chat_type'], id=msgdetail["chat_id"],
                        name=msgdetail["chat_name"], date=msgdetail["datefor"], memberid=msgdetail["who_id"])

        if msgdetail["name"]:
            # Hardcode 'private' as message might have been received in a
            # chat but we still want to update user information
            updatestats(type='private', id=msgdetail["who_id"],
                        name=msgdetail["name"], date=msgdetail["datefor"],
                        memberid=msgdetail["chat_id"])

    if text:
        if text.split()[0].lower()[0:6] == "/stats":
            statscommands(message)
        elif text.split()[0].lower()[0:7] == "/getout":
            getoutcommands(message)

    if "@all" in text:
        getall(message)

    leftchat = False
    try:
        if 'left_chat_participant' in message['message']:
            leftchat = True
    except:
        leftchat = False

    if leftchat:
        chat_id = msgdetail["chat_id"]
        try:
            wholeft = message['message']['left_chat_participant']['id']
        except:
            wholeft = False

        if wholeft:
            logger.debug(msg=_L('Someone with id %s left chat %s, cleaning up') % (wholeft, msgdetail["chat_name"]))

            remove_from_memberid(type=msgdetail['chat_type'], id=chat_id, memberid=wholeft)
            remove_from_memberid(type='private', id=wholeft, memberid=chat_id)

        # Check if it was the bot leaving the channel and cleanup
        try:
            leftusername = message['message']['left_chat_participant']['username']
        except:
            leftusername = False

        if leftusername:
            try:
                botname = stampy.stampy.getme()
            except:
                botname = False

            if leftusername == botname:
                # Bot has been removed from chat, full cleanup of chat data
                logger.debug(msg=_L('Bot has left chat %s, cleaning up') % msgdetail["chat_name"])
                dochatcleanup(chat_id=chat_id, maxage=0)
    migrate = False
    try:
        if 'migrate_to_chat_id' in message['message']:
            migrate = True
    except:
        migrate = False

    if migrate:
        # Chat has been migrated to superchat, so we can migrate all configuration
        chat_id = msgdetail['chat_id']
        new_id = message['message']['migrate_to_chat_id']
        migratechats(oldchat=chat_id, newchat=new_id)

    return


def help(message):  # do not edit this line
    """
    Returns help for plugin
    :param message: message to process
    :return: help text
    """

    commandtext = _("Use `@all` to ping all users in a channel as long as they have username defined in Telegram\n\n")
    if stampy.stampy.is_owner(message):
        commandtext += _("Use `/stats show <private|group|supergroup|channel|search>` to get stats on last usage\n\n")
        commandtext += _("Use `/getout <chatid|here>` to have bot leave that chat or current one\n\n")
    return commandtext


def statscommands(message):
    """
    Processes stats commands in the messages
    :param message: message to process
    :return:
    """

    logger = logging.getLogger(__name__)

    msgdetail = stampy.stampy.getmsgdetail(message)

    texto = msgdetail["text"]
    chat_id = msgdetail["chat_id"]
    message_id = msgdetail["message_id"]
    who_un = msgdetail["who_un"]

    if stampy.stampy.is_owner(message):
        logger.debug(msg=_L("Owner Stat: %s by %s") % (texto, who_un))
        try:
            command = texto.split(' ')[1]
        except:
            command = False

        try:
            key = texto.split(' ')[2]
        except:
            key = ""

        for case in stampy.stampy.Switch(command):
            if case('show'):
                text = showstats(type=key)
                stampy.stampy.sendmessage(chat_id=chat_id, text=text,
                                          reply_to_message_id=message_id,
                                          disable_web_page_preview=True,
                                          parse_mode="Markdown")
                break
            if case('purge'):
                dochatcleanup()
                dousercleanup()

                break

            if case('search'):
                text = showstats(name=key)
                stampy.stampy.sendmessage(chat_id=chat_id, text=text,
                                          reply_to_message_id=message_id,
                                          disable_web_page_preview=True,
                                          parse_mode="Markdown")

                break

            if case('usearch'):
                text = showstats(name=key, key='memberid')
                stampy.stampy.sendmessage(chat_id=chat_id, text=text,
                                          reply_to_message_id=message_id,
                                          disable_web_page_preview=True,
                                          parse_mode="Markdown")

                break

            if case():
                break

    return


def getoutcommands(message):
    """
    Processes getout commands in the messages
    :param message: message to process
    :return:
    """

    logger = logging.getLogger(__name__)

    msgdetail = stampy.stampy.getmsgdetail(message)

    texto = msgdetail["text"]
    chat_id = msgdetail["chat_id"]
    who_un = msgdetail["who_un"]

    if stampy.stampy.is_owner(message):
        logger.debug(msg=_L("Owner getout: %s by %s") % (texto, who_un))
        try:
            command = texto.split(' ')[1]
        except:
            command = False

        if command == 'here':
            command = chat_id

        if command:
            try:
                getoutofchat(chat_id=command)
            except:
                pass
    return


def showstats(type=False, name=None, key="name"):
    """
    Shows stats for defined type or all if missing
    :param name: name to search in the stats database
    :param type: user or chat or empy for combined
    :return: table with the results
    """
    logger = logging.getLogger(__name__)
    if type:
        sql = "select type,id,name,date,count from stats WHERE type='%s'" % type

        if name:
            string = "%" + "%s" % name + "%"
            sql = sql + " and " + key + "like '%s'" % string
    else:
        sql = "select type,id,name,date,count from stats"

        if name:
            string = "%" + "%s" % name + "%"
            sql = sql + " WHERE " + key + " like '%s'" % string

    sql = sql + " ORDER BY count DESC LIMIT 10"

    cur = stampy.stampy.dbsql(sql)
    table = from_db_cursor(cur)
    text = _("Defined stats:\n")
    text = "%s\n```%s```" % (text, table.get_string())
    logger.debug(msg=_L("Returning stats %s") % text)
    return text


def updatestats(type=False, id=0, name=False, date=False, memberid=None):
    """
    Updates count stats for a given type
    :param type: user or chat
    :param id: ID to update
    :param name: name of the chat of user
    :param date: date of the update
    :param memberid: ID of the origin to add
        chat_id if type='user'
        user_id if type='chat'
    :return:
    """

    logger = logging.getLogger(__name__)

    try:
        value = getstats(type=type, id=id)
        count = value[4] + 1
    except:
        value = False
        count = 0

    if value:
        newmemberid = value[5]
    else:
        newmemberid = []

    # Only add the id if it was not already stored
    if memberid not in newmemberid:
        if memberid is list:
            newmemberid.extend(memberid)
        else:
            newmemberid.append(memberid)

    newmemberid = stampy.stampy.getitems(newmemberid)

    if 'False' in newmemberid:
        newmemberid.remove('False')
    if 'false' in newmemberid:
        newmemberid.remove('false')
    if False in newmemberid:
        newmemberid.remove(False)
    if "" in newmemberid:
        newmemberid.remove("")
    if [] in newmemberid:
        newmemberid.remove([])

    sql = "DELETE from stats where id='%s'" % id
    stampy.stampy.dbsql(sql)

    sql = "INSERT INTO stats(type, id, name, date, count, memberid) VALUES('%s', '%s', '%s', '%s', '%s', '%s');" % (type, id, name, date, count, json.dumps(newmemberid))

    logger.debug(msg=_L("values: type:%s, id:%s, name:%s, date:%s, count:%s, memberid: %s") % (type, id, name, date, count, newmemberid))

    if id:
        try:
            stampy.stampy.dbsql(sql)
        except:
            logger.debug(msg=_L("ERROR on updatestats"))
    return


def remove_from_memberid(type=False, id=0, name=False, date=False, memberid=None):
    """
    Remove memberID from memberid
    :param type: user or chat
    :param id: ID to update
    :param name: name of the chat of user
    :param date: date of the update
    :param memberid: ID of the origin to remove
        chat_id if type='user'
        user_id if type='chat'
    :return:
    """

    logger = logging.getLogger(__name__)

    try:
        value = getstats(type=type, id=id)
        count = value[4] + 1
    except:
        value = False
        count = 0

    if value:
        newmemberid = value[5]
    else:
        newmemberid = []

    # Only add the id if it was not already stored
    if memberid in newmemberid:
        newmemberid.remove(memberid)

    newmemberid = stampy.stampy.getitems(newmemberid)

    if 'False' in newmemberid:
        newmemberid.remove('False')
    if 'false' in newmemberid:
        newmemberid.remove('false')
    if False in newmemberid:
        newmemberid.remove(False)
    if "" in newmemberid:
        newmemberid.remove("")
    if [] in newmemberid:
        newmemberid.remove([])

    sql = "DELETE from stats where id='%s'" % id
    stampy.stampy.dbsql(sql)

    sql = "INSERT INTO stats(type, id, name, date, count, memberid) VALUES('%s', '%s', '%s', '%s', '%s', '%s');" % (type, id, name, date, count, json.dumps(newmemberid))

    logger.debug(msg=_L("values: type:%s, id:%s, name:%s, date:%s, count:%s, memberid: %s") % (type, id, name, date, count, newmemberid))

    if id:
        try:
            stampy.stampy.dbsql(sql)
        except:
            logger.debug(msg=_L("ERROR on remove_from_memberid"))
    return


def getchatmemberscount(chat_id=False):
    """
    Get number of users in the actual chat_id
    :param chat_id: Channel ID to query for the number of users
    :return: number of members in chat ID.
    """

    logger = logging.getLogger(__name__)
    url = "%s%s/getChatMembersCount?chat_id=%s" % (stampy.plugin.config.config(key='url'),
                                                   stampy.plugin.config.config(key='token'),
                                                   chat_id)
    try:
        result = str(json.load(urllib.urlopen(url))['result'])
    except:
        result = 0

    logger.info(msg=_L("Chat id %s users %s") % (chat_id, result))
    return result


def getoutofchat(chat_id=False):
    """
    Use API call to get the bot out of chats
    :param: chat_id: Channel ID to leave
    """

    logger = logging.getLogger(__name__)
    url = "%s%s/leaveChat?chat_id=%s" % (stampy.plugin.config.config(key='url'),
                                         stampy.plugin.config.config(key='token'),
                                         chat_id)
    try:
        result = str(json.load(urllib.urlopen(url))['result'])
    except:
        result = 0

    logger.info(msg=_L("Chat id %s left") % chat_id)
    return result


def dochatcleanup(chat_id=False, maxage=False):
    """
    Checks on the stats database the date of the last update in the chat
    :param chat_id: Channel ID to query in database
    :param maxage: defines maximum number of days to allow chats to be inactive
    """

    logger = logging.getLogger(__name__)

    if chat_id:
        sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type <> 'private' and id=%s" % chat_id
    else:
        sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type <> 'private'"

    chatids = []
    cur = stampy.stampy.dbsql(sql)

    for row in cur:
        chatid = row[1]
        chatids.append(chatid)

    logger.debug(msg=_L("Processing chat_ids for cleanup: %s") % chatids)

    for chatid in chatids:
        (type, id, name, date, count, memberid) = getstats(id=chatid)
        if date and (date != "False"):
            chatdate = datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
        else:
            chatdate = datetime.datetime.now()

        now = datetime.datetime.now()

        # get maxage from channel config (to quickly expire some) or general
        maxage = int(stampy.plugin.config.gconfig("maxage", default=180))

        if (now - chatdate).days >= maxage:
            logger.debug(msg=_L("CHAT ID %s with name %s with %s inactivity days is going to be purged") % (
                chatid, name, (now - chatdate).days))
            # The last update was older than maxage days ago, get out of chat and
            #  remove karma
            texto = _("Due to inactivity of more than %s days, this bot will exit the channel, please re-add in the future if needed") % maxage
            stampy.stampy.sendmessage(chatid, text=texto)

            getoutofchat(chatid)

            # Check if this channel was master to another, if so, elect new
            # master, and update karma, autokarma, alias, quote to the new
            # master

            newmaster = 0
            maxmembers = 0

            sql = "SELECT id from config WHERE key='link' and value='%s'" % chatid
            cur = stampy.stampy.dbsql(sql)

            for row in cur.fetchall():
                id = row[0]
                value = getstats(id=id)
                if value:
                    newmemberid = value[5]
                else:
                    newmemberid = []

                if len(newmemberid) > maxmembers:
                    maxmembers = len(newmemberid)
                    newmaster = id

            if newmaster != 0:
                logger.debug(msg=_L("The removed channel (%s) was master for others, electing new master: %s") % (chatid, newmaster))
                # Update slaves to new master
                sql = "UPDATE config SET value='%s' WHERE key='link' and value='%s'" % (newmaster, chatid)
                cur = stampy.stampy.dbsql(sql)

                migratechats(oldchat=chat_id, newchat=newmaster, includeall=False)

                # Remove 'link' from the new master so it becomes a master
                stampy.plugin.config.deleteconfig(key='link', gid=newmaster)

            # Two different names because of historical reasons
            for table in ['config', 'stats']:
                # Remove channel stats
                sql = "DELETE from %s where id='%s';" % (table, chatid)
                cur = stampy.stampy.dbsql(sql)

            for table in ['karma', 'quote', 'autokarma', 'alias', 'feeds']:
                # Remove channel stats
                sql = "DELETE from %s where gid='%s';" % (table, chatid)
                cur = stampy.stampy.dbsql(sql)

            # Remove users membership that had that channel id
            string = "%" + "%s" % chatid + "%"
            sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type='user' and memberid LIKE '%s';" % string
            cur = stampy.stampy.dbsql(sql)

            for line in cur:
                (type, id, name, date, count, memberid) = line
                logger.debug(msg=_L("LINE for user %s and memberid: %s will be deleted") % (name, memberid))
                try:
                    memberid.remove(chatid)
                except:
                    pass
                # Update stats entry in database without the removed chat
                updatestats(type=type, id=id, name=name, date=date, memberid=memberid)
    return


def migratechats(oldchat, newchat, includeall=True):
    """
    Updates chat references
    :param includeall: defines if stats and config should we moved (chat->supergroup)
    :param oldchat: Old chat id
    :param newchat: Newer chat id
    :return:
    """
    logger = logging.getLogger(__name__)

    # move data from old master to new one (except stats and config)
    logger.debug(msg=_L("Migrating chat id: %s to %s") % (oldchat, newchat))
    for table in ['karma', 'quote', 'autokarma', 'alias', 'feeds']:
        sql = "UPDATE %s SET gid='%s' where gid='%s';" % (table, newchat, oldchat)
        stampy.stampy.dbsql(sql)

    if includeall:
        for table in ['config', 'stats']:
            sql = "UPDATE %s SET id='%s' where id='%s';" % (table, newchat, oldchat)
            stampy.stampy.dbsql(sql)

        # Migrate forward data
        sql = "UPDATE forward SET source='%s' where source='%s';" % (newchat, oldchat)
        stampy.stampy.dbsql(sql)
        sql = "UPDATE forward SET target='%s' where target='%s';" % (newchat, oldchat)
        stampy.stampy.dbsql(sql)
    else:
        # Delete forwards not migrated
        sql = "DELETE FROM forward WHERE source='%s';" % oldchat
        stampy.stampy.dbsql(sql)
        sql = "DELETE FROM forward WHERE target='%s';" % oldchat
        stampy.stampy.dbsql(sql)
    return


def dousercleanup(user_id=False, maxage=int(stampy.plugin.config.config("maxage", default=180))):
    """
    Checks on the stats database the date of the last update from the user
    :param user_id: User ID to query in database
    :param maxage: defines maximum number of days to allow chats to be inactive
    """

    logger = logging.getLogger(__name__)

    if user_id:
        sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type='user' and id=%s" % user_id
    else:
        sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type='user'"

    userids = []
    cur = stampy.stampy.dbsql(sql)

    for row in cur:
        userid = row[1]
        userids.append(userid)

    logger.debug(msg=_L("Processing userids for cleanup: %s") % userids)

    for userid in userids:
        (type, id, name, date, count, memberid) = getstats(type='user',
                                                           id=userid)
        if date and (date != "False"):
            chatdate = datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
        else:
            chatdate = datetime.datetime.now()

        now = datetime.datetime.now()

        if (now - chatdate).days > maxage:
            logger.debug(msg=_L("USER ID %s with name %s with %s inactivity days is going to be purged") % (
                userid, name, (now - chatdate).days))

            # Remove channel stats
            sql = "DELETE from stats where id='%s';" % userid
            cur = stampy.stampy.dbsql(sql)

            # Remove hilights
            sql = "DELETE from hilight where gid='%s';" % userid
            cur = stampy.stampy.dbsql(sql)

            # Remove users membership that had that channel id
            sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE type<>'private' and memberid LIKE '%%%s%%';" % userid
            cur = stampy.stampy.dbsql(sql)

            for line in cur:
                (type, id, name, date, count, memberid) = line
                logger.debug(msg=_L("LINE for user %s and memberid: %s will be deleted") % (name, memberid))
                memberid.remove(userid)
                # Update stats entry in database without the removed chat
                updatestats(type=type, id=id, name=name, date=date, memberid=memberid)

            # Check if user was admin for any channel, and remove
            username = None
            if name:
                for each in name.split():
                    if "@" in each:
                        username = each[1:-1]
            if username and username != "@":
                # userid to remove has username, check admins on config and remove
                string = "%" + username + "%"
                sql = "SELECT id, value FROM config WHERE value like %s" % string

                cur = stampy.stampy.dbsql(sql)

                for row in cur:
                    id = row[0]
                    admins = row[1].split(" ")
                    try:
                        admins.remove(username)
                    except:
                        pass
                    newadmin = " ".join(admins)

                    if len(newadmin) != 0:
                        stampy.plugin.config.setconfig(key='admin',
                                                       value=newadmin, gid=id)
                    else:
                        stampy.plugin.config.deleteconfig(key='admin', gid=id)

    return


def getstats(type=False, id=0, name=False, date=False, count=0):
    """
    Gets statistics for specified element
    :param type: chat or user type to query
    :param id: identifier for user or chat
    :param name: full name
    :param date: date
    :param count: number of messages
    :return: (type, id, name, date, count, memberid)
    """

    logger = logging.getLogger(__name__)
    sql = "SELECT type,id,name,date,count,memberid FROM stats WHERE id='%s'" % id
    if type:
        sql = "%s%s" % (sql, " AND type='%s';" % type)
    cur = stampy.stampy.dbsql(sql)
    try:
        value = cur.fetchone()
    except:
        value = False

    memberid = []
    if value:
        (type, id, name, date, count, oldmemberid) = value
        try:
            memberid = json.loads(oldmemberid)
        except:
            memberid = []

    if not count:
        count = 0

    logger.debug(msg=_L("values: type:%s, id:%s, name:%s, date:%s, count:%s, memberid:%s") % (type, id, name, date, count, memberid))

    # Ensure we return the modified values
    return type, id, name, date, count, memberid


def getall(message):
    """
    Processes 'all' in messages
    :param message:  message to analyze
    :return:
    """
    logger = logging.getLogger(__name__)
    msgdetail = stampy.stampy.getmsgdetail(message)

    texto = msgdetail["text"]
    chat_id = msgdetail["chat_id"]
    message_id = msgdetail["message_id"]
    who_un = msgdetail["who_un"]

    if "@all" in texto:
        logger.debug(msg=_L("@All invoked"))
        (type, id, name, date, count, members) = getstats(id=chat_id)

        all = []
        for member in members:
            (type, id, name, date, count, memberid) = getstats(type='private', id=member)
            username = None
            if name:
                for each in name.split():
                    if "@" in each:
                        username = each[1:-1]
            if username and username != "@":
                all.append(username)

        if "@all++" in texto:
            text = ""
            newall = []
            for each in all:
                newall.append("%s++" % each)
            text += " ".join(newall)
            msgdetail["text"] = text
            if newall and text:
                stampy.plugin.karma.karmaprocess(msgdetail)
        else:
            text = _("%s wanted to ping you: ") % who_un
            if text and all:
                text += " ".join(all)
                stampy.stampy.sendmessage(chat_id=chat_id, text=text,
                                          reply_to_message_id=message_id,
                                          disable_web_page_preview=True)
    return


def pingchat(chatid):
    """
    Updates chat modification time
    :param chatid: Chat to update in stats database
    :return:
    """

    logger = logging.getLogger(__name__)
    (type, id, name, date, count, memberid) = getstats(id=chatid)
    date = datetime.datetime.now()
    datefor = date.strftime('%Y-%m-%d %H:%M:%S')
    logger.debug(msg=_L("Pinging chat %s: %s on %s") % (chatid, name, datefor))
    updatestats(type=type, id=chatid, name=name,
                date=datefor, memberid=memberid)
    return


def idfromuser(idorname=False, chat_id=False):
    logger = logging.getLogger(__name__)
    string = "%" + "%s" % idorname + "%"
    sql = "select id,name from stats where (name like '%s' or id like '%s')" % (string, string)

    if chat_id:
        string = "%" + "%s" % chat_id + "%"
        sql = sql + " and memberid like '%s'" % string

    sql = sql + ";"

    # Find user ID provided in database for current channel

    cur = stampy.stampy.dbsql(sql)
    results = []

    for row in cur:
        # Process each word returned
        results.append({"id": row[0], "name": row[1]})

    logger.debug(msg=_L("Found users with id(%s)/chat(%s): %s") % (idorname, chat_id, results))

    return results


def getusers(chat_id=False):
    """
    Checks on the stats database list of users
    :param chat_id: Channel ID to query in database
    """

    logger = logging.getLogger(__name__)

    userids = []
    if chat_id:
        (type, id, name, date, count, members) = getstats(id=chat_id)
        for member in members:
            (type, id, name, date, count, memberid) = getstats(type='private', id=member)
            userids.append(id)

    logger.debug(msg=_L("Returning userids: %s for chat: %s") % (userids,
                                                                 chat_id))
    return userids