From 4c655f382099fcfbd0a03b984c2e1e24cee4d2db Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Sun, 4 Sep 2016 18:27:52 +0200 Subject: [PATCH 1/7] Implemented username to UID conversion persistence --- .../event_handlers/telegram_handler.py | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) mode change 100755 => 100644 pokemongo_bot/event_handlers/telegram_handler.py diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py old mode 100755 new mode 100644 index 32a4df9c10..dfdfc6e9c5 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -7,6 +7,8 @@ import telegram import thread import re +from pokemongo_bot.datastore import Datastore + DEBUG_ON = False @@ -19,7 +21,35 @@ class TelegramClass: def __init__(self, bot, master, pokemons, config): self.bot = bot - self.master = master + with self.bot.database as conn: + # initialize the DB table if it does not exist yet + cur = conn.cursor() + #cur.execute("create table if not exists telegram_uids(uid integer, username text) primary key(uid)") + # create indexes if they don't exist + #cur.execute("create index if not exists tuids_username on telegram_uids(username)") + + try: + cur.executescript(""" + create table if not exists telegram_uids(uid integer constraint upk primary key, username text not null); + create index if not exists tuids_username on telegram_uids(username); + """) + except sqlite3.Error as e: + print "An error occurred:", e.args[0] + + + # if master is not numeric, try to fetch it from the database + if re.match(r'^[0-9]+$', master): # master is numeric + self.master = master + else: + c = conn.cursor() + # do we already have a user id? + c.execute("SELECT uid from telegram_uids where username in ('{}', '@{}')".format(master, master)) + results = c.fetchall() + if len(results) > 0: # woohoo, we already saw a message from this master and therefore have a uid + self.master = results[0][0] + else: # uid not known yet + self.master = master + self.pokemons = pokemons self._tbot = None self.config = config @@ -84,27 +114,36 @@ def send_player_stats_to_chat(self, chat_id): self.sendLocation(chat_id=chat_id, latitude=self.bot.api._position_lat, longitude=self.bot.api._position_lng) else: self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="Stats not loaded yet\n") + def grab_uid(self, update): + with self.bot.database as conn: + conn.execute("replace into telegram_uids (uid, username) values (?, ?)", (update.message.chat_id, update.message.from_user.username)) + conn.commit() + def run(self): time.sleep(1) while True: for update in self._tbot.getUpdates(offset=self.update_id, timeout=10): self.update_id = update.update_id+1 if update.message: - self.bot.logger.info("message from {} ({}): {}".format(update.message.from_user.username, update.message.from_user.id, update.message.text)) + self.bot.logger.info("Telegram message from {} ({}): {}".format(update.message.from_user.username, update.message.from_user.id, update.message.text)) if not self.master: # Reject message if no master defined in config - outMessage = "Telegram bot setup not yet complete (master = null). Please enter your userid {} into bot configuration file.".format(update.message.from_user.id) + outMessage = "Telegram bot setup not yet complete (master = null). Please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask).".format(update.message.from_user.id) self.bot.logger.warn(outMessage) continue if self.master not in [update.message.from_user.id, "@{}".format(update.message.from_user.username)]: # Reject message if sender does not match defined master in config - outMessage = "Telegram message received from unknown sender. If this was you, please enter your userid {} as master in bot configuration file.".format(update.message.from_user.id) + outMessage = "Telegram message received from unknown sender. If this was you, please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask).".format(update.message.from_user.id, update.message.from_user.username) self.bot.logger.warn(outMessage) continue if self.master and not re.match(r'^[0-9]+$', str(self.master)): + outMessage = "Telegram message received from correct user, but master is not numeric, updating datastore." + self.bot.logger.warn(outMessage) # the "master" is not numeric, set self.master to update.message.chat_id and re-instantiate the handler newconfig = self.config newconfig['master'] = update.message.chat_id + # insert chat id into database + self.grab_uid(update) # remove old handler self.bot.event_manager._handlers = filter(lambda x: not isinstance(x, TelegramHandler), self.bot.event_manager._handlers) # add new handler (passing newconfig as parameter) From 8b78bab1fae058184b3f9f47a6ff22cb73f13fad Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Sun, 4 Sep 2016 18:44:11 +0200 Subject: [PATCH 2/7] removed debug code --- pokemongo_bot/event_handlers/telegram_handler.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py index dfdfc6e9c5..9c124446c0 100644 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -24,17 +24,13 @@ def __init__(self, bot, master, pokemons, config): with self.bot.database as conn: # initialize the DB table if it does not exist yet cur = conn.cursor() - #cur.execute("create table if not exists telegram_uids(uid integer, username text) primary key(uid)") - # create indexes if they don't exist - #cur.execute("create index if not exists tuids_username on telegram_uids(username)") - try: cur.executescript(""" create table if not exists telegram_uids(uid integer constraint upk primary key, username text not null); create index if not exists tuids_username on telegram_uids(username); """) except sqlite3.Error as e: - print "An error occurred:", e.args[0] + self.bot.logger.warn("An error occurred while initializing Telegram UID table: {}".format(e.args[0])) # if master is not numeric, try to fetch it from the database From 1705398b50c6f8e4b7fa1aff1529bf88525fd2c8 Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Sun, 4 Sep 2016 23:25:46 +0200 Subject: [PATCH 3/7] fixed bugs --- .../event_handlers/telegram_handler.py | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py index 9c124446c0..01845ed520 100644 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -26,7 +26,7 @@ def __init__(self, bot, master, pokemons, config): cur = conn.cursor() try: cur.executescript(""" - create table if not exists telegram_uids(uid integer constraint upk primary key, username text not null); + create table if not exists telegram_uids(uid text constraint upk primary key, username text not null); create index if not exists tuids_username on telegram_uids(username); """) except sqlite3.Error as e: @@ -34,16 +34,21 @@ def __init__(self, bot, master, pokemons, config): # if master is not numeric, try to fetch it from the database - if re.match(r'^[0-9]+$', master): # master is numeric + if re.match(r'^[0-9]+$', str(master)): # master is numeric self.master = master + self.bot.logger.info("Telegram master is valid (numeric): {}".format(master)) else: + self.bot.logger.info("Telegram master is not numeric: {}".format(master)) c = conn.cursor() # do we already have a user id? - c.execute("SELECT uid from telegram_uids where username in ('{}', '@{}')".format(master, master)) + srchmaster = re.sub(r'^@', '', master) + c.execute("SELECT uid from telegram_uids where username in ('{}', '@{}')".format(srchmaster, srchmaster)) results = c.fetchall() if len(results) > 0: # woohoo, we already saw a message from this master and therefore have a uid + self.bot.logger.info("Telegram master UID from datastore: {}".format(results[0][0])) self.master = results[0][0] else: # uid not known yet + self.bot.logger.info("Telegram master UID not in datastore yet") self.master = master self.pokemons = pokemons @@ -127,9 +132,9 @@ def run(self): outMessage = "Telegram bot setup not yet complete (master = null). Please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask).".format(update.message.from_user.id) self.bot.logger.warn(outMessage) continue - if self.master not in [update.message.from_user.id, "@{}".format(update.message.from_user.username)]: + if str(self.master) not in [str(update.message.from_user.id), "@{}".format(update.message.from_user.username), update.message.from_user.username]: # Reject message if sender does not match defined master in config - outMessage = "Telegram message received from unknown sender. If this was you, please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask).".format(update.message.from_user.id, update.message.from_user.username) + outMessage = "Telegram message received from unknown sender. If this was you, please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask); current value there: {}.".format(update.message.from_user.id, update.message.from_user.username, self.master) self.bot.logger.warn(outMessage) continue if self.master and not re.match(r'^[0-9]+$', str(self.master)): @@ -159,34 +164,57 @@ class TelegramHandler(EventHandler): def __init__(self, bot, config): self.bot = bot self.tbot = None - self.master = config.get('master', None) + master = config.get('master', None) self.pokemons = config.get('alert_catch', {}) self.whoami = "TelegramHandler" self.config = config + if master == None: + self.master = None + return + + with self.bot.database as conn: + # if master is not numeric, try to fetch it from the database + if not re.match(r'^[0-9]+$', str(master)): # master is numeric + self.bot.logger.info("Telegram master is not numeric: {}".format(master)) + c = conn.cursor() + # do we already have a user id? + srchmaster = re.sub(r'^@', '', master) + c.execute("SELECT uid from telegram_uids where username in ('{}', '@{}')".format(srchmaster, srchmaster)) + results = c.fetchall() + if len(results) > 0: # woohoo, we already saw a message from this master and therefore have a uid + self.bot.logger.info("Telegram master UID from datastore: {}".format(results[0][0])) + self.master = results[0][0] + else: # uid not known yet + self.bot.logger.info("Telegram master UID not in datastore yet") + self.master = master def handle_event(self, event, sender, level, formatted_msg, data): if self.tbot is None: try: + self.bot.logger.info("Telegram bot not running, trying to spin it up") self.tbot = TelegramClass(self.bot, self.master, self.pokemons, self.config) self.tbot.connect() thread.start_new_thread(self.tbot.run) except Exception as inst: self.tbot = None + self.bot.logger.error("Unable to spin Telegram bot; master: {}".format(self.master)) return if self.master: if not re.match(r'^[0-9]+$', str(self.master)): + # master not numeric?... + self.bot.logger.info("Telegram master not numeric: {}".format(self.master, type(self.master))) return master = self.master if event == 'level_up': msg = "level up ({})".format(data["current_level"]) elif event == 'pokemon_caught': - if isinstance(self.pokemons, list): + if isinstance(self.pokemons, list): # alert_catch is a plain list if data["pokemon"] in self.pokemons or "all" in self.pokemons: msg = "Caught {} CP: {}, IV: {}".format(data["pokemon"], data["cp"], data["iv"]) else: return - else: + else: # alert_catch is a dict if data["pokemon"] in self.pokemons: trigger = self.pokemons[data["pokemon"]] elif "all" in self.pokemons: From dcadcae313abd4b82d33cdd30f12cafdb574d64c Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Tue, 6 Sep 2016 12:47:32 +0200 Subject: [PATCH 4/7] New dynamic TelegramTask notifications --- docs/telegramtask.md | 36 +++ .../event_handlers/telegram_handler.py | 241 +++++++++++++++--- 2 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 docs/telegramtask.md diff --git a/docs/telegramtask.md b/docs/telegramtask.md new file mode 100644 index 0000000000..aed50c5b99 --- /dev/null +++ b/docs/telegramtask.md @@ -0,0 +1,36 @@ +TelegramTask configuration and subscriptions for updates + + +**Authentication** +There are two ways to be authenticated with the Telegram task of the bot: +* authentication for one predefined userid or username (config parameter: "master", can be given as a number (userid) or as a string(username); for username, please note that it is case-sensitive); this will automatically authenticate all requests coming from the particular userid/username (note: when providing username, you will need to send a message to the bot in order for the bot to learn your user id). Hardcoded notifications will only be sent to this username/userid. +* authentication by a password (config parameter: "password"): this will wait for a "/login " command to be sent to the bot and will from then on treat the corresponding userid as authenticated until a "/logout" command is sent from that user id. + +**Hardcoded notifications** +Certain notifications (egg_hatched, bot_sleep, spin_limit, catch_limit, level_up) will always be sent to the "master" and cannot be further configured. *Please consider this feature deprecated, it will be removed in the future* +The notification for pokemon_caught can be configured by using the "alert_catch" configuration parameter. This parameter can be: +* either a plain list (in this case, a notification will be sent for every pokemon on the list caught, with "all" matching all pokemons) +* or a "key: value" dict of the following form +> "pokemon_name": {"operator": "and/or", "cp": cpmin, "iv": ivmin } +Again, "all" will apply to all pokemons. If a matching pokemon is caught, depending on "operator" being "and" or "or", a notification will be sent if both or one of the criteria is met. +Example: +> "Dratini": { "operator": "and", "cp": 1200, "iv": 0.99 } +This will send a notification if a Dratini is caught with at least 1200CP and at least 0.99 potential. + +**Dynamic notifications(subscriptions)** +Every authenticated user can subscribe to be notified in case a certain event is emitted. The list of currently available events can be retrieved by sending "/events" command. + +In order to subscribe to a certain event, e.g. to "no_pokeballs", you simply send the "/sub" command as follows: +> /sub no_pokeballs +In order to remove this subscription: +> /unsub no_pokeballs +*Note: the /unsub command must match exactly the corresponding /sub command* +A special case is "/sub all" - it will subscribe you to all events. Be prepared for huge amount of events! "/unsub all" will remove this subscription, without changing other subscriptions. +Another special case is "/unsub everything" - this will remove all your subscriptions. +Currently, only pokemon_caught event can be configured with more detail, here is an example: +> /sub pokemon_caught operator:and cp:1200 pokemon:Dratini iv:0.99 +This will subscribe you to be notified every time a Dratini has been caught with cp equal or higher than 1200 and iv equal or higher than 0.99 (same as in "Hardcoded notifications" above) + +*/showsubs* will show your current subscriptions. + + diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py index 4614850ff1..24eace42f1 100644 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -8,6 +8,7 @@ import thread import re from pokemongo_bot.datastore import Datastore +import pprint DEBUG_ON = False @@ -19,22 +20,17 @@ class TelegramClass: update_id = None + def __init__(self, bot, master, pokemons, config): self.bot = bot with self.bot.database as conn: # initialize the DB table if it does not exist yet - cur = conn.cursor() - try: - cur.executescript(""" - create table if not exists telegram_uids(uid text constraint upk primary key, username text not null); - create index if not exists tuids_username on telegram_uids(username); - """) - except sqlite3.Error as e: - self.bot.logger.warn("An error occurred while initializing Telegram UID table: {}".format(e.args[0])) - + initiator = TelegramDBInit(bot.database) + if master == None: # no master supplied + self.master = master # if master is not numeric, try to fetch it from the database - if re.match(r'^[0-9]+$', str(master)): # master is numeric + elif unicode(master).isnumeric(): # master is numeric self.master = master self.bot.logger.info("Telegram master is valid (numeric): {}".format(master)) else: @@ -120,6 +116,52 @@ def grab_uid(self, update): conn.execute("replace into telegram_uids (uid, username) values (?, ?)", (update.message.chat_id, update.message.from_user.username)) conn.commit() + def isMasterFromConfigFile(self, chat_id): + if not hasattr(self, "master") or not self.master: + return False + if unicode(self.master).isnumeric(): + return unicode(chat_id) == unicode(self.master) + else: + with self.bot.database as conn: + cur = conn.cursor() + cur.execute("select username from telegram_uids where uid = ?", [chat_id]) + res = cur.fetchone() + return res != None and unicode(res[0]) == unicode(re.replace(r'^@', '', self.master)) + + def isMasterFromActiveLogins(self, chat_id): + with self.bot.database as conn: + cur = conn.cursor() + cur.execute("select count(1) from telegram_logins where uid = ?", [chat_id]) + res = cur.fetchone() + if res[0] == 1: + return True + else: + return False + + def isAuthenticated(self, chat_id): + return self.isMasterFromConfigFile(chat_id) or self.isMasterFromActiveLogins(chat_id) + + def deauthenticate(self, update): + with self.bot.database as conn: + cur = conn.cursor() + cur.execute("delete from telegram_logins where uid = ?", [update.message.chat_id]) + conn.commit() + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Logout completed") + return + + def authenticate(self, update): + (command, password) = update.message.text.split(' ') + if password != self.config.get('password', None): + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Invalid password") + else: + with self.bot.database as conn: + cur = conn.cursor() + cur.execute("delete from telegram_logins where uid = ?", [update.message.chat_id]) + cur.execute("insert into telegram_logins(uid) values(?)", [update.message.chat_id]) + conn.commit() + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Authentication successful, you can now use all commands") + return + def run(self): time.sleep(1) while True: @@ -127,17 +169,28 @@ def run(self): self.update_id = update.update_id+1 if update.message: self.bot.logger.info("Telegram message from {} ({}): {}".format(update.message.from_user.username, update.message.from_user.id, update.message.text)) - if not self.master: - # Reject message if no master defined in config - outMessage = "Telegram bot setup not yet complete (master = null). Please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask).".format(update.message.from_user.id) - self.bot.logger.warn(outMessage) + if update.message.text == "/start" or update.message.text == "/help": + res = ( + "Commands: ", + "/info - info about bot", + "/login - authenticate with the bot; once authenticated, your ID will be registered with the bot and survive bot restarts", + "/logout - remove your ID from the 'authenticated' list", + "/sub [] - subscribe to event_name, with optional parameters, event_name=all will subscribe to ALL events (LOTS of output!)", + "/unsub [] - unsubscribe from event_name; parameters must match the /sub parameters", + "/unsub everything - will remove all subscriptions for this uid", + "/showsubs - show current subscriptions", + "/events - show available events" + ) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="\n".join(res)) continue - if str(self.master) not in [str(update.message.from_user.id), "@{}".format(update.message.from_user.username), update.message.from_user.username]: - # Reject message if sender does not match defined master in config - outMessage = "Telegram message received from unknown sender. If this was you, please enter your userid ({}) or your username (@{}) as master in bot configuration file (config section of TelegramTask); current value there: {}.".format(update.message.from_user.id, update.message.from_user.username, self.master) - self.bot.logger.warn(outMessage) + + if self.config.get('password', None) == None and not self.master: # no auth provided in config + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="No password nor master configured in TelegramTask section, bot will not accept any commands") continue - if self.master and not re.match(r'^[0-9]+$', str(self.master)): + if re.match(r'^/login [^ ]+', update.message.text): + self.authenticate(update) + continue + if not self.isAuthenticated(update.message.from_user.id) and hasattr(self, "master") and self.master and not unicode(self.master).isnumeric() and unicode(self.master) == unicode(update.message.from_user.username): outMessage = "Telegram message received from correct user, but master is not numeric, updating datastore." self.bot.logger.warn(outMessage) # the "master" is not numeric, set self.master to update.message.chat_id and re-instantiate the handler @@ -149,19 +202,94 @@ def run(self): self.bot.event_manager._handlers = filter(lambda x: not isinstance(x, TelegramHandler), self.bot.event_manager._handlers) # add new handler (passing newconfig as parameter) self.bot.event_manager.add_handler(TelegramHandler(self.bot, newconfig)) + continue + if not self.isAuthenticated(update.message.from_user.id): + # Reject message if sender does not match defined master in config + outMessage = "Telegram message received from unknown sender. Please either make sure your username or ID is in TelegramTask/master, or a password is set in TelegramTask section and /login is issued" + self.bot.logger.error(outMessage) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Please /login first") + continue + # one way or another, the user is now authenticated if update.message.text == "/info": self.send_player_stats_to_chat(update.message.chat_id) - elif update.message.text == "/start" or update.message.text == "/help": - res = ( - "Commands: ", - "/info - info about bot" - ) - self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="\n".join(res)) - else: - self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Unrecognized command: {}".format(update.message.text)) + continue + if update.message.text == "/events": + self.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', text=(", ".join(self.bot.event_manager._registered_events.keys()))) + continue + if update.message.text == "/logout": + self.deauthenticate(update) + continue + if re.match(r'^/sub ', update.message.text): + self.chsub(update.message.text, update.message.chat_id) + continue + if re.match(r'^/unsub ', update.message.text): + self.chsub(update.message.text, update.message.chat_id) + continue + if re.match(r'^/showsubs', update.message.text): + self.showsubs(update.message.chat_id) + continue + + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Unrecognized command: {}".format(update.message.text)) + def showsubs(self, chatid): + subs = [] + with self.bot.database as conn: + for sub in conn.execute("select uid, event_type, parameters from telegram_subscriptions where uid = ?", [chatid]).fetchall(): + subs.append("{} -> {}".format(sub[1], sub[2])) + self.sendMessage(chat_id=chatid, parse_mode='HTML', text="\n".join(subs)) + + def chsub(self, msg, chatid): + (cmd, evt, params) = self.tokenize(msg, 3) + if cmd == "/sub": + sql = "replace into telegram_subscriptions(uid, event_type, parameters) values (?, ?, ?)" + else: + if evt == "everything": + sql = "delete from telegram_subscriptions where uid = ? and (event_type = ? or parameters = ? or 1 = 1)" # does not look very elegant, but makes unsub'ing everythign possible + else: + sql = "delete from telegram_subscriptions where uid = ? and event_type = ? and parameters = ?" + + with self.bot.database as conn: + conn.execute(sql, [chatid, evt, params]) + conn.commit() + return + + def tokenize(self, string, maxnum): + spl = string.split(' ', maxnum-1) + while len(spl) < maxnum: + spl.append(" ") + return spl + +class TelegramDBInit: + def __init__(self, conn): + self.conn = conn + self.initDBstructure() + return + + def initDBstructure(self): + db_structure = { + "telegram_uids": "CREATE TABLE telegram_uids(uid text constraint upk primary key, username text not null)", + "tuids_username": "CREATE INDEX tuids_username on telegram_uids(username)", + "telegram_logins": "CREATE TABLE telegram_logins(uid text constraint tlupk primary key, logindate integer(4) default (strftime('%s', 'now')))", + "telegram_subscriptions": "CREATE TABLE telegram_subscriptions(uid text, event_type text, parameters text, constraint tspk primary key(uid, event_type, parameters))", + "ts_uid": "CREATE INDEX ts_uid on telegram_subscriptions(uid)" + } + for objname in db_structure: + self.initDBobject(objname, db_structure[objname]) + return + + def initDBobject(self, name, sql): + res = self.conn.execute("select sql,type from sqlite_master where name = ?", [name]).fetchone() # grab objects definition + + if len(res) > 0 and res[0] != sql: # object exists and sql not matching + self.conn.execute("drop {} {}".format(res[1], name)) # drop it + + if len(res) == 0 or res[0] != sql: # object missing or sql not matching + self.conn.execute(sql) + return + class TelegramHandler(EventHandler): def __init__(self, bot, config): + initiator = TelegramDBInit(bot.database) self.bot = bot self.tbot = None master = config.get('master', None) @@ -174,7 +302,7 @@ def __init__(self, bot, config): with self.bot.database as conn: # if master is not numeric, try to fetch it from the database - if not re.match(r'^[0-9]+$', str(master)): # master is numeric + if not unicode(master).isnumeric(): self.bot.logger.info("Telegram master is not numeric: {}".format(master)) c = conn.cursor() # do we already have a user id? @@ -188,21 +316,68 @@ def __init__(self, bot, config): self.bot.logger.info("Telegram master UID not in datastore yet") self.master = master + def catch_notify(self, pokemon, cp, iv, params): + if params == " ": + return True + try: + oper = re.search(r'operator:([^ ]+)', params).group(1) + rule_cp = int(re.search(r'cp:([0-9]+)', params).group(1)) + rule_iv = float(re.search(r'iv:([0-9.]+)', params).group(1)) + rule_pkmn = re.search(r'pokemon:([^ ]+)', params).group(1) + return rule_pkmn == pokemon and (oper == "or" and (cp >= rule_cp or iv >= rule_iv) or cp >= rule_cp and iv >= rule_iv) + except: + return False + + + def handle_event(self, event, sender, level, formatted_msg, data): if self.tbot is None: try: + if hasattr(self, "master"): + selfmaster = self.master + else: + selfmaster = None self.bot.logger.info("Telegram bot not running, trying to spin it up") - self.tbot = TelegramClass(self.bot, self.master, self.pokemons, self.config) + self.tbot = TelegramClass(self.bot, selfmaster, self.pokemons, self.config) self.tbot.connect() thread.start_new_thread(self.tbot.run) except Exception as inst: self.tbot = None - self.bot.logger.error("Unable to spin Telegram bot; master: {}".format(self.master)) + self.bot.logger.error("Unable to spin Telegram bot; master: {}, exception: {}".format(selfmaster, pprint.pformat(inst))) return - if self.master: - if not re.match(r'^[0-9]+$', str(self.master)): + # prepare message to send + if event == 'level_up': + msg = "level up ({})".format(data["current_level"]) + elif event == 'pokemon_caught': + msg = "Caught {} CP: {}, IV: {}".format(data["pokemon"], data["cp"], data["iv"]) + elif event == 'egg_hatched': + msg = "Egg hatched with a {} CP: {}, IV: {}".format(data["pokemon"], data["cp"], data["iv"]) + elif event == 'bot_sleep': + msg = "I am too tired, I will take a sleep till {}.".format(data["wake"]) + elif event == 'catch_limit': + msg = "*You have reached your daily catch limit, quitting.*" + elif event == 'spin_limit': + msg = "*You have reached your daily spin limit, quitting.*" + else: + msg = formatted_msg + + # first handle subscriptions; they are independent of master setting. + with self.bot.database as conn: + subs = conn.execute("select uid, parameters, event_type from telegram_subscriptions where event_type in (?,'all','debug')", [event]).fetchall() + for sub in subs: + (uid, params, event_type) = sub + if event != 'pokemon_caught' or self.catch_notify(data["pokemon"], int(data["cp"]), float(data["iv"]), params): + if event_type == "debug": + self.bot.logger.info("[{}] {}".format(event, msg)) + else: + self.tbot.sendMessage(chat_id=uid, parse_mode='Markdown', text=msg) + + + + if hasattr(self, "master") and self.master: + if not unicode(self.master).isnumeric(): # master not numeric?... - self.bot.logger.info("Telegram master not numeric: {}".format(self.master, type(self.master))) + # cannot send event notifications to non-numeric master (yet), so quitting return master = self.master From 172785a8b530841ec38f2692b7cd2e52ae7b4036 Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Tue, 6 Sep 2016 12:57:57 +0200 Subject: [PATCH 5/7] Update telegramtask.md --- docs/telegramtask.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/telegramtask.md b/docs/telegramtask.md index aed50c5b99..941a29d998 100644 --- a/docs/telegramtask.md +++ b/docs/telegramtask.md @@ -2,35 +2,44 @@ TelegramTask configuration and subscriptions for updates **Authentication** + There are two ways to be authenticated with the Telegram task of the bot: -* authentication for one predefined userid or username (config parameter: "master", can be given as a number (userid) or as a string(username); for username, please note that it is case-sensitive); this will automatically authenticate all requests coming from the particular userid/username (note: when providing username, you will need to send a message to the bot in order for the bot to learn your user id). Hardcoded notifications will only be sent to this username/userid. -* authentication by a password (config parameter: "password"): this will wait for a "/login " command to be sent to the bot and will from then on treat the corresponding userid as authenticated until a "/logout" command is sent from that user id. +* authentication for one predefined userid or username (config parameter: `master`, can be given as a number (userid) or as a string(username); for username, please note that it is case-sensitive); this will automatically authenticate all requests coming from the particular userid/username (note: when providing username, you will need to send a message to the bot in order for the bot to learn your user id). Hardcoded notifications will only be sent to this username/userid. +* authentication by a password (config parameter: `password`): this will wait for a `/login ` command to be sent to the bot and will from then on treat the corresponding userid as authenticated until a `/logout` command is sent from that user id. + +**Hardcoded notifications - please consider this feature deprecated, it will be removed in the future** -**Hardcoded notifications** -Certain notifications (egg_hatched, bot_sleep, spin_limit, catch_limit, level_up) will always be sent to the "master" and cannot be further configured. *Please consider this feature deprecated, it will be removed in the future* +Certain notifications (egg_hatched, bot_sleep, spin_limit, catch_limit, level_up) will always be sent to the "master" and cannot be further configured. The notification for pokemon_caught can be configured by using the "alert_catch" configuration parameter. This parameter can be: * either a plain list (in this case, a notification will be sent for every pokemon on the list caught, with "all" matching all pokemons) * or a "key: value" dict of the following form + > "pokemon_name": {"operator": "and/or", "cp": cpmin, "iv": ivmin } + Again, "all" will apply to all pokemons. If a matching pokemon is caught, depending on "operator" being "and" or "or", a notification will be sent if both or one of the criteria is met. Example: > "Dratini": { "operator": "and", "cp": 1200, "iv": 0.99 } + This will send a notification if a Dratini is caught with at least 1200CP and at least 0.99 potential. **Dynamic notifications(subscriptions)** + Every authenticated user can subscribe to be notified in case a certain event is emitted. The list of currently available events can be retrieved by sending "/events" command. -In order to subscribe to a certain event, e.g. to "no_pokeballs", you simply send the "/sub" command as follows: +In order to subscribe to a certain event, e.g. to "no_pokeballs", you simply send the `/sub` command as follows: > /sub no_pokeballs + In order to remove this subscription: > /unsub no_pokeballs -*Note: the /unsub command must match exactly the corresponding /sub command* -A special case is "/sub all" - it will subscribe you to all events. Be prepared for huge amount of events! "/unsub all" will remove this subscription, without changing other subscriptions. -Another special case is "/unsub everything" - this will remove all your subscriptions. + +*Note: the `/unsub` command must match exactly the corresponding `/sub` command* +A special case is `/sub all` - it will subscribe you to all events. Be prepared for huge amount of events! `/unsub all` will remove this subscription, without changing other subscriptions. +Another special case is `/unsub everything` - this will remove all your subscriptions. Currently, only pokemon_caught event can be configured with more detail, here is an example: > /sub pokemon_caught operator:and cp:1200 pokemon:Dratini iv:0.99 + This will subscribe you to be notified every time a Dratini has been caught with cp equal or higher than 1200 and iv equal or higher than 0.99 (same as in "Hardcoded notifications" above) -*/showsubs* will show your current subscriptions. +`/showsubs` will show your current subscriptions. From 8c9b38cdb523ccedd4c30bc82dbb454ab779b9d6 Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Tue, 6 Sep 2016 13:00:20 +0200 Subject: [PATCH 6/7] Update configuration_files.md --- docs/configuration_files.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/configuration_files.md b/docs/configuration_files.md index 1c613fcc11..008d0673e3 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -1000,8 +1000,9 @@ Bot answer on command '/info' self stats. ### Options * `telegram_token` : bot token (getting [there](https://core.telegram.org/bots#6-botfather) - one token per bot) -* `master` : id (without quotes) of bot owner, who will get alerts and may issue commands. +* `master` : id (without quotes) of bot owner, who will get alerts and may issue commands or a (case-sensitive!) user name. * `alert_catch` : dict of rules pokemons catch. +* `password` : a password to be used to authenticate to the bot The bot will only alert and respond to a valid master. If you're unsure what this is, send the bot a message from Telegram and watch the log to find out. @@ -1016,7 +1017,8 @@ The bot will only alert and respond to a valid master. If you're unsure what thi "alert_catch": { "all": {"operator": "and", "cp": 1300, "iv": 0.95}, "Snorlax": {"operator": "or", "cp": 900, "iv": 0.9} - } + }, + "password": "alwoefhq348" } } ``` @@ -1062,4 +1064,4 @@ Available `team` : "team": 2 } } -``` \ No newline at end of file +``` From f12deb8d4364a4d539ddad0b591fa700ec5e7a4e Mon Sep 17 00:00:00 2001 From: DBa2016 Date: Tue, 6 Sep 2016 13:06:56 +0200 Subject: [PATCH 7/7] some more error handling --- pokemongo_bot/event_handlers/telegram_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py index 24eace42f1..f37dac67e5 100644 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -184,7 +184,7 @@ def run(self): self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="\n".join(res)) continue - if self.config.get('password', None) == None and not self.master: # no auth provided in config + if self.config.get('password', None) == None and (not hasattr(self, "master") or not self.master): # no auth provided in config self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="No password nor master configured in TelegramTask section, bot will not accept any commands") continue if re.match(r'^/login [^ ]+', update.message.text):