def getPlugins(): """ Gets list of plugins in the plugins folder :return: list of plugins available """ __name__ = 'stampy.stampy.plugins' logger = logging.getLogger(__name__) plugins = [] possibleplugins = os.listdir(PluginFolder) for i in possibleplugins: if i != "__init__.py" and os.path.splitext(i)[1] == ".py": i = os.path.splitext(i)[0] try: info = imp.find_module(i, [PluginFolder]) except: info = False if i and info: if i not in plugin.config.config(key='disabled_plugins', default=''): logger.debug(msg=_L("Plugin added: %s") % i) plugins.append({"name": i, "info": info}) else: logger.debug(msg=_L("Plugin disabled: %s") % i) return plugins
def sendimage(chat_id=0, image="", text="", reply_to_message_id=""): """ Sends an image to chat_id as a reply to a message received :param chat_id: ID of the chat :param image: image URI :param text: Additional text or caption :param reply_to_message_id: :return: """ logger = logging.getLogger(__name__) if not image: return False url = "%s%s/sendPhoto" % (plugin.config.config(key='url'), plugin.config.config(key='token')) payload = {'chat_id': chat_id} if reply_to_message_id: payload['reply_to_message_id'] = reply_to_message_id if text: payload['caption'] = text.encode('utf-8') logger.debug(msg=_L("Sending image: %s") % text) # Download image first to later send it rawimage = requests.get(image, stream=True) sent = False if rawimage.status_code == 200: rawimage.raw.decode_content = True # Send image files = {'photo': rawimage.raw} # It this is executed as per unit testing, skip sending message UTdisable = not plugin.config.config(key='unittest', default=False) Silent = not plugin.config.gconfig( key='silent', default=False, gid=geteffectivegid(gid=chat_id)) try: if UTdisable and Silent: output = requests.post(url, files=files, data=payload) sent = {"message": json.loads(output.text)['result']} else: sent = False except: logger.debug(msg=_L("Failure sending image: %s") % image) sent = False else: logger.debug(msg=_L("Failure downloading image: %s") % image) # Check if there's something to forward and do it plugin.forward.forwardmessage(sent) return sent
def getme(): """ Gets bot user :return: returns the items obtained """ logger = logging.getLogger(__name__) if not plugin.config.config(key='myself', default=False): url = "%s%s/getMe" % (plugin.config.config(key='url'), plugin.config.config(key='token')) message = "%s" % url try: result = json.load(urllib.urlopen(message))['result']['username'] except: result = 'stampy' plugin.config.setconfig(key='myself', value=result) else: result = plugin.config.config(key='myself') logger.info(msg=_L("Getting bot details and returning: %s") % result) return result
def getitems(var): """ Returns list of items even if provided args are lists of lists :param var: list or value to pass :return: unique list of values """ logger = logging.getLogger(__name__) result = [] if not isinstance(var, list): result.append(var) else: for elem in var: result.extend(getitems(elem)) # Do cleanup of duplicates final = [] for elem in result: if elem not in final: final.append(elem) # As we call recursively, don't log calls for just one ID if len(final) > 1: logger.debug(msg=_L("Final deduplicated list: %s") % final) return final
def sendsticker(chat_id=0, sticker="", text="", reply_to_message_id=""): """ Sends a sticker to chat_id as a reply to a message received :param chat_id: ID of the chat :param sticker: Sticker identification :param text: Additional text :param reply_to_message_id: :return: """ logger = logging.getLogger(__name__) url = "%s%s/sendSticker" % (plugin.config.config(key='url'), plugin.config.config(key='token')) message = "%s?chat_id=%s" % (url, chat_id) message = "%s&sticker=%s" % (message, sticker) if reply_to_message_id: message += "&reply_to_message_id=%s" % reply_to_message_id logger.debug(msg=_L("Sending sticker: %s") % text) # It this is executed as per unit testing, skip sending message UTdisable = not plugin.config.config(key='unittest', default=False) Silent = not plugin.config.gconfig( key='silent', default=False, gid=geteffectivegid(gid=chat_id)) if UTdisable and Silent: sent = {"message": json.load(urllib.urlopen(message))['result']} else: sent = False # Check if there's something to forward and do it plugin.forward.forwardmessage(sent) return
def is_owner_or_admin(message, strict=False): """ Check if user is owner or admin for group :param strict: Defines if we target the actual gid, not effective :param message: message to check :return: True on owner or admin """ logger = logging.getLogger(__name__) admin = False owner = False msgdetail = getmsgdetail(message) if strict: chat_id = msgdetail["chat_id"] else: chat_id = geteffectivegid(msgdetail["chat_id"]) # if we're on a user private chat, return admin true if chat_id > 0: admin = True logger.debug(msg=_L("We're admin of private chats")) else: # Check if we're owner owner = is_owner(message) if not owner: logger.debug(msg=_L("We're not owner of public chats")) # Check if we are admin of chat for each in plugin.config.config(key='admin', default="", gid=chat_id).split(" "): if each == msgdetail["who_un"]: admin = True logger.debug(msg=_L("We're admin of public chat")) # If we're not admin and admin is empty, consider ourselves admin if not admin: if plugin.config.config(key='admin', gid=chat_id, default="") == "": logger.debug(msg=_L( "We're admin because no admin listed on public chat")) admin = True return owner or admin
def shouldrun(name): """ Checks name on database to see if it should run or not and updates as executed :param name: Name to check on database :return: Bool """ sql = "SELECT name,lastchecked,interval from cron where name='%s'" % name cur = dbsql(sql) date = utize(datetime.datetime.now()) # Formatted date to write back in database datefor = date.strftime('%Y/%m/%d %H:%M:%S') # Convert to epoch to properly compare dateforts = time.mktime(date.timetuple()) code = False for row in cur: (name, lastchecked, interval) = row try: # Parse date or if in error, use past datelast = utize(dateutil.parser.parse(lastchecked)) except: datelast = utize(datetime.datetime(year=1981, month=1, day=24)) # Get time since last check on the feed (epoch) datelastts = time.mktime(datelast.timetuple()) timediff = int((dateforts - datelastts) / 60) # Check if interval is defined or set default interval = int(interval) if interval == 0: interval = 1440 # If more time has passed since last check than the interval for # checks, run the check if timediff < interval: code = False else: code = True # Update db with results if code: sql = "UPDATE cron SET lastchecked='%s' where name='%s'" % (datefor, name) logger.debug(msg=_L("Updating last checked as per %s") % sql) dbsql(sql=sql) return code
def initplugins(): """ Initializes plugins :return: list of plugin modules initialized """ __name__ = 'stampy.stampy.plugins' logger = logging.getLogger(__name__) plugs = [] plugtriggers = {} for i in getPlugins(): logger.debug(msg=_L("Processing plugin initialization: %s") % i["name"]) newplug = loadPlugin(i) plugs.append(newplug) triggers = [] for each in newplug.init(): triggers.append(each) plugtriggers[i["name"]] = triggers logger.debug(msg=_L("Plugin %s is triggered by %s") % (i["name"], triggers)) return plugs, plugtriggers
def processcron(): """ This function processes plugins with cron features """ logger = logging.getLogger(__name__) # Call plugins to process message global plugs global plugtriggers for i in plugs: name = i.__name__.split(".")[-1] if shouldrun(name=name): logger.debug(msg=_L("Processing plugin cron: %s") % name) i.cron()
def clearupdates(offset): """ Marks updates as already processed so they are removed by API :param offset: :return: """ logger = logging.getLogger(__name__) url = "%s%s/getUpdates" % (plugin.config.config(key='url'), plugin.config.config(key='token')) message = "%s?" % url message += "offset=%s&" % offset try: result = json.load(urllib.urlopen(message)) except: result = False logger.info(msg=_L("Clearing messages at %s") % offset) return result
def getupdates(offset=0, limit=100): """ Gets updates (new messages from server) :param offset: last update id :param limit: maximum number of messages to gather :return: returns the items obtained """ logger = logging.getLogger(__name__) url = "%s%s/getUpdates" % (plugin.config.config(key='url'), plugin.config.config(key='token')) message = "%s?" % url if offset != 0: message += "offset=%s&" % offset message += "limit=%s" % limit try: result = json.load(urllib.urlopen(message))['result'] except: result = [] for item in result: logger.info(msg=_L("Getting updates and returning: %s") % item) yield item
def createorupdatedb(): """ Create database if it doesn't exist or upgrade it to head :return: """ logger = logging.getLogger(__name__) import alembic.config alembicArgs = [ '-x', 'database=%s' % options.database, '--raiseerr', 'upgrade', 'head', ] logger.debug(msg=_L( "Using alembic to upgrade/create database to expected revision")) alembic.config.main(argv=alembicArgs) return
def loglevel(): """ This functions stores or sets the proper log level based on the database configuration """ logger = logging.getLogger(__name__) level = False for case in Switch(plugin.config.config(key="verbosity").lower()): # choices=["info", "debug", "warn", "critical"]) if case('debug'): level = logging.DEBUG break if case('critical'): level = logging.CRITICAL break if case('warn'): level = logging.WARN break if case('info'): level = logging.INFO break if case(): # Default to DEBUG log level level = logging.DEBUG # If logging level has changed, redefine in logger, # database and send message if logging.getLevelName( logger.level).lower() != plugin.config.config(key="verbosity"): logger.setLevel(level) logger.info(msg=_L("Logging level set to %s") % plugin.config.config(key="verbosity")) plugin.config.setconfig(key="verbosity", value=logging.getLevelName( logger.level).lower())
attempt = attempt + 1 try: cur.execute(sql) con.commit() worked = True attempt = 60 except: exc_info = sys.exc_info() traceback.print_exception(*exc_info) worked = False sleep(random.randint(0, 10)) else: attempt = 60 if not worked: logger.critical(msg=_L("Error # %s on SQL execution: %s") % (attempt, sql)) return cur def sendmessage(chat_id=0, text="", reply_to_message_id=False, disable_web_page_preview=True, parse_mode=False, extra=False): """ Sends a message to a chat :param chat_id: chat_id to receive the message :param text: message text
def sendmessage(chat_id=0, text="", reply_to_message_id=False, disable_web_page_preview=True, parse_mode=False, extra=False): """ Sends a message to a chat :param chat_id: chat_id to receive the message :param text: message text :param reply_to_message_id: message_id to reply :param disable_web_page_preview: do not expand links to include preview :param parse_mode: use specific format (markdown, html) :param extra: extra parameters to send (for future functions like keyboard_markup) :return: """ logger = logging.getLogger(__name__) url = "%s%s/sendMessage" % (plugin.config.config(key="url"), plugin.config.config(key='token')) lines = text.split("\n") maxlines = 15 if len(lines) > maxlines: # message might be too big for single message (max 4K) if "```" in text: markdown = True else: markdown = False texto = string.join(lines[0:maxlines], "\n") if markdown: texto = "%s```" % texto # Send first batch sendmessage(chat_id=chat_id, text=texto, reply_to_message_id=reply_to_message_id, disable_web_page_preview=disable_web_page_preview, parse_mode=parse_mode, extra=extra) # Send remaining batch texto = string.join(lines[maxlines:], "\n") if markdown: texto = "```%s" % texto sendmessage(chat_id=chat_id, text=texto, reply_to_message_id=False, disable_web_page_preview=disable_web_page_preview, parse_mode=parse_mode, extra=extra) return overridegid = plugin.config.config(key='overridegid', gid=0, default=False) if overridegid: chat_id = overridegid message = "%s?chat_id=%s&text=%s" % ( url, chat_id, urllib.quote_plus(text.encode('utf-8'))) if reply_to_message_id: message += "&reply_to_message_id=%s" % reply_to_message_id if disable_web_page_preview: message += "&disable_web_page_preview=1" if parse_mode: message += "&parse_mode=%s" % parse_mode if extra: message += "&%s" % extra code = False attempt = 0 while not code: # It this is executed as per unit testing, skip sending message UTdisable = not plugin.config.config(key='unittest', default=False) Silent = not plugin.config.gconfig( key='silent', default=False, gid=geteffectivegid(gid=chat_id)) if UTdisable and Silent: result = json.load(urllib.urlopen(message)) code = result['ok'] else: code = True result = "" if attempt > 0: logger.error( msg=_L("ERROR (%s) sending message: Code: %s : Text: %s") % (attempt, code, result)) attempt += 1 sleep(1) if not code: error = result['description'] if 'entity starting at byte offset' in error: # Trim the byte offset to make this error more generic error = error[:103] for case in Switch(error): if case(u"Bad Request: message text is empty"): # Message is empty, no need to resend attempt = 61 break if case(u"Forbidden: bot can't initiate conversation with a user" ): # Bot hasn't been authorized by user, cancelling attempt = 61 break if case(u"Bad Request: reply message not found"): # Original reply has been deleted attempt = 61 break if case(u"Forbidden: bot was blocked by the user"): # User blocked the bot attempt = 61 break if case(u"Bad Request: can't parse entities in message text: Can't find end of the entity starting at byte offset" ): attempt = 61 break if case(u'Forbidden: bot was kicked from the supergroup chat'): attempt = 61 break if case(u'Bad Request: chat not found'): attempt = 61 break # exit after 60 retries with 1 second delay each if attempt > 60: logger.error( msg=_L("PERM ERROR sending message: Code: %s : Text: %s") % (code, result)) code = True try: sent = {"message": result['result']} except: sent = False if sent: # Check if there's something to forward and do it plugin.forward.forwardmessage(sent) logger.debug(msg=_L("Sending message: Code: %s : Text: %s") % (code, text)) return code
def process(messages): """ This function processes the updates in the Updates URL at Telegram for finding commands, karma changes, config, etc """ logger = logging.getLogger(__name__) # check if Log level has changed loglevel() # Main code for processing the karma updates date = 0 lastupdateid = 0 count = 0 # Process each message available in URL and search for karma operators for message in messages: # Count messages in each batch count += 1 # Forward message if defined plugin.forward.forwardmessage(message) # Call plugins to process message global plugs global plugtriggers msgdetail = getmsgdetail(message) botname = getme() # Write the line for debug messageline = _L("TEXT: %s : %s : %s") % ( msgdetail["chat_name"], msgdetail["name"], msgdetail["text"]) logger.debug(msg=messageline) # Process group configuration for language chat_id = msgdetail['chat_id'] chlang(lang=plugin.config.gconfig(key='language', gid=chat_id)) try: command = msgdetail["text"].split()[0].lower().replace( '@%s' % botname, '') texto = msgdetail["text"].lower() date = msgdetail["datefor"] except: command = "" texto = "" date = 0 for i in plugs: name = i.__name__.split(".")[-1] runplugin = False for trigger in plugtriggers[name]: if "*" in trigger: runplugin = True break elif trigger[0] == "^": if command == trigger[1:]: runplugin = True break elif trigger in texto: runplugin = True break code = False if runplugin: logger.debug(msg=_L("Processing plugin: %s") % name) code = i.run(message=message) if code: # Plugin has changed triggers, reload plugtriggers[name] = i.init() logger.debug(msg=_L("New triggers for %s: %s") % (name, plugtriggers[name])) # Update last message id to later clear it from the server if msgdetail["update_id"] > lastupdateid: lastupdateid = msgdetail["update_id"] if date != 0: logger.info(msg=_L("Last processed message at: %s") % date) if lastupdateid != 0: logger.debug(msg=_L("Last processed update_id : %s") % lastupdateid) if count != 0: logger.info(msg=_L("Number of messages in this batch: %s") % count) # clear updates (marking messages as read) if lastupdateid != 0: clearupdates(offset=lastupdateid + 1)
def main(): """ Main code for the bot """ # Main code logger = logging.getLogger(__name__) # Set database name in config if options.database: createorupdatedb() plugin.config.setconfig(key='database', value=options.database) # Configure logging conflogging(target="stampy") # Configuring alembic logger conflogging(target="alembic") logger.info(msg=_L("Started execution")) if not plugin.config.config(key='sleep'): plugin.config.setconfig(key='sleep', value=10) # Remove our name so it is retrieved on boot plugin.config.deleteconfig(key='myself') # Check if we've the token required to access or exit if not plugin.config.config(key='token'): if options.token: token = options.token plugin.config.setconfig(key='token', value=token) else: msg = _( "Token required for operation, please check https://core.telegram.org/bots" ) logger.critical(msg) sys.exit(1) # Check if we've URL defined on DB or on cli and store if not plugin.config.config(key='url'): if options.url: plugin.config.setconfig(key='url', value=options.url) # Check if we've owner defined in DB or on cli and store if not plugin.config.config(key='owner'): if options.owner: plugin.config.setconfig(key='owner', value=options.owner) # Initialize modules global plugs global plugtriggers plugs, plugtriggers = plugins.initplugins() logger.debug(msg=_L("Plug triggers reported: %s") % plugtriggers) # Check operation mode and call process as required if options.daemon or plugin.config.config(key='daemon'): plugin.config.setconfig(key='daemon', value=True) logger.info(msg=_L("Running in daemon mode")) while plugin.config.config(key='daemon') == 'True': process(getupdates()) processcron() sleep(int(plugin.config.config(key='sleep'))) else: logger.info(msg=_L("Running in one-shoot mode")) process(getupdates()) processcron() logger.info(msg=_L("Stopped execution")) logging.shutdown() sys.exit(0)