def limit_reached(update): if Cfg.get("rate_limit", "enabled"): if update.message: uid = update.message.from_user.id cmd = update.message.text.split(" ")[0] elif update.inline_query: uid = update.effective_user.id cmd = update.inline_query.query[:-1].split(" ")[0] else: uid = cmd = None if not Cfg.get("rate_limit", "incl_cmd"): cmd = None rate = Cfg.get("rate_limit", "requests") time = Cfg.get("rate_limit", "timespan") if RateLimit.reached(uid, rate, time, command=cmd): msg = f"{emo.NO_ENTRY} Rate limit ({rate} requests in " \ f"{time} seconds) exceeded. Wait a few seconds..." update.message.reply_text(msg) return True return False else: return False
def _update_check(self): def _check_for_update(bot, job): user = Cfg.get('update', 'github_user') repo = Cfg.get('update', 'github_repo') gh = GitHub(github_user=user, github_repo=repo) try: # Get latest release response = gh.get_latest_release() except Exception as ex: logging.error(repr(ex)) return if job.context: if job.context["tag"] == response["tag_name"]: return else: job.context = dict() job.context["tag"] = response["tag_name"] release_notes = response["body"] try: response = gh.get_tags() except Exception as ex: logging.error(repr(ex)) return new_hash = str() for t in response: if t["name"] == job.context["tag"]: new_hash = t["commit"]["sha"] break cfg_hash = Cfg.get("update", "update_hash") if cfg_hash != new_hash: for admin in Cfg.get("admin_id"): update_cmd = utl.esc_md("/update") tag = job.context['tag'] bot.send_message( admin, f"New release *{tag}* available\n\n" f"*Release Notes*\n{release_notes}\n\n" f"{update_cmd}", parse_mode=ParseMode.MARKDOWN) if Cfg.get("update", "update_check") is not None: sec = utl.get_seconds(Cfg.get("update", "update_check")) if not sec: sec = con.DEF_UPDATE_CHECK msg = f"Update check time not valid. Using {sec} seconds" logging.warning(msg) try: self.job_queue.run_repeating(_check_for_update, sec, first=0) except Exception as e: logging.error(repr(e))
def bot_start_webhook(self): self.updater.start_webhook(listen=Cfg.get("webhook", "listen"), port=Cfg.get("webhook", "port"), url_path=self.token, key=Cfg.get("webhook", "privkey_path"), cert=Cfg.get("webhook", "cert_path"), webhook_url=f"{Cfg.get('webhook', 'url')}:" f"{Cfg.get('webhook', 'port')}/" f"{self.token}")
def _refresh_cache(self): if Cfg.get("refresh_cache") is not None: sec = utl.get_seconds(Cfg.get("refresh_cache")) if not sec: sec = con.DEF_CACHE_REFRESH msg = f"Refresh rate for caching not valid. Using {sec} seconds" logging.warning(msg) try: self.job_queue.run_repeating(APICache.refresh, sec, first=0) except Exception as e: logging.error(repr(e))
def _check_for_update(bot, job): user = Cfg.get('update', 'github_user') repo = Cfg.get('update', 'github_repo') gh = GitHub(github_user=user, github_repo=repo) try: # Get latest release response = gh.get_latest_release() except Exception as ex: logging.error(repr(ex)) return if job.context: if job.context["tag"] == response["tag_name"]: return else: job.context = dict() job.context["tag"] = response["tag_name"] release_notes = response["body"] try: response = gh.get_tags() except Exception as ex: logging.error(repr(ex)) return new_hash = str() for t in response: if t["name"] == job.context["tag"]: new_hash = t["commit"]["sha"] break cfg_hash = Cfg.get("update", "update_hash") if cfg_hash != new_hash: for admin in Cfg.get("admin_id"): update_cmd = utl.esc_md("/update") tag = job.context['tag'] try: bot.send_message( admin, f"New release *{utl.esc_md(tag)}* available\n\n" f"*Release Notes*\n{utl.esc_md(release_notes)}\n\n" f"{update_cmd}", parse_mode=ParseMode.MARKDOWN) except Exception as ex: err = f"Can't send release notes to chat {admin}" logging.error(f"{err} - {ex}")
def get_action(self, bot, update, args): msg = f"{emo.WAIT} Restarting bot..." m = update.message.reply_text(msg) user_id = update.effective_user.id cls_name = type(self).__name__.lower() Cfg.set(user_id, "plugins", cls_name, "user_id") Cfg.set(m.message_id, "plugins", cls_name, "message") m_name = __spec__.name m_name = m_name[:m_name.index(".")] time.sleep(0.2) os.execl(sys.executable, sys.executable, '-m', m_name, *sys.argv[1:])
def __init__(self, telegram_bot): super().__init__(telegram_bot) # Check if database is enabled since this plugin needs it if not Cfg.get("database", "use_db"): msg = f"Plugin '{type(self).__name__}' " \ f"can't be used since database is disabled" logging.warning(msg)
def __init__(self, bot_token, bot_db): self.db = bot_db self.token = bot_token read_timeout = Cfg.get("telegram", "read_timeout") connect_timeout = Cfg.get("telegram", "connect_timeout") kwargs = dict() if read_timeout: kwargs["read_timeout"] = read_timeout if connect_timeout: kwargs["connect_timeout"] = connect_timeout try: self.updater = Updater(bot_token, request_kwargs=kwargs) except InvalidToken as e: cls_name = f"Class: {type(self).__name__}" logging.error(f"{repr(e)} - {cls_name}") exit("ERROR: Bot token not valid") self.job_queue = self.updater.job_queue self.dispatcher = self.updater.dispatcher # Load classes in folder 'plugins' self._load_plugins() # Handler for files downloads (plugins) mh = MessageHandler(Filters.document, self._download) self.dispatcher.add_handler(mh) # Handler for command-links self._add_link_handler() # Handler for inline-mode inline_handler = InlineQueryHandler(self._inline) self.dispatcher.add_handler(inline_handler) # Handle all Telegram related errors self.dispatcher.add_error_handler(self._handle_tg_errors) # Refresh cache periodically self._refresh_cache() # Check for updates periodically self._update_check()
def _restart_notification(self): cls_name = type(self).__name__.lower() message = Cfg.get("plugins", cls_name, "message") user_id = Cfg.get("plugins", cls_name, "user_id") if not message or not user_id: return try: self.tgb.updater.bot.edit_message_text( chat_id=user_id, message_id=message, text=f"{emo.CHECK} Restarting bot...") except Exception as e: msg = "Not possible to update restart message" logging.error(f"{msg}: {e}") Cfg.remove("plugins", cls_name)
def after_plugins_loaded(self): if Cfg.get("database", "use_db"): for repeater in self.tgb.db.read_rep() or []: interval = repeater[4] update = repeater[5] try: self._run_repeater(update, int(interval)) except Exception as e: update.message.reply_text(text=f"{emo.ERROR} {e}", parse_mode=ParseMode.MARKDOWN)
def _save_data(self, bot, update, **kwargs): if Cfg.get("database", "use_db"): if update.message: usr = update.message.from_user cmd = update.message.text cht = update.message.chat elif update.inline_query: usr = update.effective_user cmd = update.inline_query.query[:-1] cht = update.effective_chat else: logging.warning(f"Can't save usage - {update}") return func(self, bot, update, **kwargs) if usr.id in Cfg.get("admin_id"): if not Cfg.get("database", "track_admins"): return func(self, bot, update, **kwargs) self.tgb.db.save(usr, cht, cmd) return func(self, bot, update, **kwargs)
def execute(self, sql, *args): if Cfg.get("database", "use_db"): con = sqlite3.connect(self._db_path) cur = con.cursor() cur.execute(sql, args) con.commit() result = cur.fetchall() con.close() return result
def __init__(self, bot_token, bot_db): self.db = bot_db self.token = bot_token read_timeout = Cfg.get("telegram", "read_timeout") connect_timeout = Cfg.get("telegram", "connect_timeout") kwargs = dict() if read_timeout: kwargs["read_timeout"] = read_timeout if connect_timeout: kwargs["connect_timeout"] = connect_timeout try: self.updater = Updater(bot_token, request_kwargs=kwargs) except InvalidToken: exit("ERROR: Bot token not valid") self.job_queue = self.updater.job_queue self.dispatcher = self.updater.dispatcher # Handler for command-links self._add_link_handler() # Load classes in folder 'plugins' self._load_plugins() # Handler for inline-mode self.dispatcher.add_handler(InlineQueryHandler(self._inline)) # Handle all Telegram related errors self.dispatcher.add_error_handler(self._handle_tg_errors) # Refresh cache periodically if enabled self._refresh_cache() # Check for updates periodically self._update_check()
def _download(self, bot, update): if update.effective_user.id not in Cfg.get("admin_id"): return name = update.message.effective_attachment.file_name file = bot.getFile(update.message.document.file_id) file.download(os.path.join(con.SRC_DIR, con.PLG_DIR, name)) try: self.reload_plugin(name.replace(".py", "")) update.message.reply_text( f"{emo.CHECK} Plugin loaded successfully") except Exception as ex: update.message.reply_text(f"{emo.ERROR} {ex}")
def get_action(self, bot, update, args): if not args: update.message.reply_text(text=f"Usage:\n{self.get_usage()}", parse_mode=ParseMode.MARKDOWN) return user = update.message.from_user if user.username: name = f"@{user.username}" else: name = user.first_name feedback = update.message.text.replace(f"/{self.get_cmd()} ", "") for admin in Cfg.get("admin_id"): bot.send_message(admin, f"Feedback from {name}: {feedback}") update.message.reply_text(f"Thanks for letting us know {emo.TOP}")
def _download(self, bot, update): # Check if in a private chat if bot.get_chat(update.message.chat_id).type != Chat.PRIVATE: return # Check if user that triggered the command is allowed to execute it if update.effective_user.id not in Cfg.get("admin_id"): return name = update.message.effective_attachment.file_name file = bot.getFile(update.message.document.file_id) file.download(os.path.join(con.SRC_DIR, con.PLG_DIR, name)) try: self.reload_plugin(name.replace(".py", "")) update.message.reply_text( f"{emo.CHECK} Plugin loaded successfully") except Exception as ex: update.message.reply_text(f"{emo.ERROR} {ex}")
def __init__(self): # Parse command line arguments self.args = self._parse_args() # Load config file Cfg(self.args.config) # Set up logging log_path = self.args.logfile log_level = self.args.loglevel self._init_logger(log_path, log_level) if Cfg.get("use_db"): # Create database sql_dir = con.SQL_DIR db_path = self.args.database self.db = Database(db_path, sql_dir) else: self.db = None # Create bot bot_token = self._get_bot_token() self.tg = TelegramBot(bot_token, self.db)
def _callback(self, bot, update): query = update.callback_query if not Cfg.get("database", "use_db"): bot.answer_callback_query(query.id, text="Database not enabled") return # Statistics - Number of Commands if query.data == "admin_cmds": data = self.tgb.db.execute_sql(self.get_sql("number_cmd")) if data["error"]: msg = data["error"] elif data["result"]: msg = f"`Commands: {data['result'][0][0]}`" else: msg = f"{emo.INFO} No data returned" bot.send_message(text=msg, chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) # Statistics - Number of Users elif query.data == "admin_usrs": data = self.tgb.db.execute_sql(self.get_sql("number_usr")) if data["error"]: msg = data["error"] elif data["result"]: msg = f"`Users: {data['result'][0][0]}`" else: msg = f"{emo.INFO} No data returned" bot.send_message(text=msg, chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) # Statistics - Command Toplist elif query.data == "admin_cmdtop": data = self.tgb.db.execute_sql(self.get_sql("cmd_top")) msg = str() if data["error"]: msg = data["error"] elif data["result"]: for row in data["result"] or []: msg += utl.esc_md(f"{row[1]} {row[0]}\n") else: msg = f"{emo.INFO} No data returned" bot.send_message(text=f"`Command Toplist:\n\n{msg}`", chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) # Statistics - Language Toplist elif query.data == "admin_langtop": data = self.tgb.db.execute_sql(self.get_sql("lang_top")) msg = str() if data["error"]: msg = data["error"] elif data["result"]: for row in data["result"] or []: msg += f"{row[1]} {row[0]}\n" else: msg = f"{emo.INFO} No data returned" bot.send_message(text=f"`Language Toplist:\n\n{msg}`", chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) # Statistics - User Toplist elif query.data == "admin_usertop": data = self.tgb.db.execute_sql(self.get_sql("user_top")) msg = str() if data["error"]: msg = data["error"] elif data["result"]: for row in data["result"] or []: msg += f"{row[1]} {row[0]}\n" else: msg = f"{emo.INFO} No data returned" bot.send_message(text=f"`User Toplist:\n\n{msg}`", chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) # Statistics - Daily Users elif query.data == "admin_userdaily": data = self.tgb.db.execute_sql(self.get_sql("user_daily")) if data["error"]: msg = data["error"] elif data["result"]: o_dict = OrderedDict() for row in data["result"] or []: date = row[0].split(" ")[0] if date not in o_dict: o_dict[date] = list() o_dict[date].append(row[1]) msg = str() for k, v in o_dict.items(): msg += f"\n{k}\n" for name in v: msg += f"{name}\n" else: msg = f"{emo.INFO} No data returned" bot.send_message(text=f"`Daily Users:\n{msg}`", chat_id=update.effective_user.id, parse_mode=ParseMode.MARKDOWN) bot.answer_callback_query(query.id, text="Query executed")
def _only_owner(self, bot, update, **kwargs): if update.effective_user.id in Cfg.get("admin_id"): return func(self, bot, update, **kwargs)
def get_action(self, bot, update, args): restart = False force = False check = False if "force" in args: force = True if "check" in args: check = True if "restart" in args: restart = True if force and check: msg = f"{emo.ERROR} Combination of 'force' " \ f"and 'check' arguments not allowed" update.message.reply_text(msg) return kw = utl.get_kw(args) branch = kw.get("branch", None) release = kw.get("release", None) if branch and release: msg = f"{emo.ERROR} Combination of 'branch' " \ f"and 'release' arguments not allowed" update.message.reply_text(msg) return msg = f"{emo.WAIT} Check for update..." m = update.message.reply_text(msg) user = Cfg.get('update', 'github_user') repo = Cfg.get('update', 'github_repo') gh = GitHub(github_user=user, github_repo=repo) uid = update.message.from_user.id download_url = str() try: # Clean old update data if present shutil.rmtree(os.path.join(os.getcwd(), con.UPD_DIR)) except: pass # ---------- BRANCH ---------- if branch: try: # Get latest commit info for branch response = gh.get_latest_branch(branch) except Exception as e: return self.handle_error(e, update) cfg_hash = Cfg.get("update", "update_hash") new_hash = response["commit"]["sha"] msg = f"{emo.CHECK} Check for update..." bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) if cfg_hash == new_hash and not force: msg = f"{emo.CHECK} You are already running the latest version" update.message.reply_text(msg) return if check: msg = f"{emo.CHECK} New branch commits available!" update.message.reply_text(msg) return # Get latest version of branch as zip download_url = f"https://github.com/{user}/{repo}/archive/{branch}.zip" # ---------- RELEASE ---------- else: try: if release: # Get specific release response = gh.get_releases() else: # Get latest release response = gh.get_latest_release() except Exception as e: return self.handle_error(e, update) if release: tag = response[0]["tag_name"] release_notes = response[0]["body"] else: tag = response["tag_name"] release_notes = response["body"] try: response = gh.get_tags() except Exception as e: return self.handle_error(e, update) new_hash = str() for t in response: if t["name"] == tag: new_hash = t["commit"]["sha"] download_url = t["zipball_url"] break if not new_hash: msg = f"{emo.ERROR} Tag '{tag}' unknown" update.message.reply_text(msg) return cfg_hash = Cfg.get("update", "update_hash") msg = f"{emo.CHECK} Check for update..." bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) if cfg_hash == new_hash and not force: msg = f"{emo.CHECK} You are already running this release" update.message.reply_text(msg) return if check: msg = f"{emo.CHECK} New release *{tag}* available!\n\n" \ f"*Release Notes*\n{release_notes}" update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) return # ---------- DOWNLOAD & UPDATE ---------- msg = f"{emo.WAIT} Downloading update..." m = update.message.reply_text(msg) try: response = requests.get(download_url) response.raise_for_status() except Exception as e: return self.handle_error(e, update) msg = f"{emo.CHECK} Downloading update..." bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) msg = f"{emo.WAIT} Updating bot..." m = update.message.reply_text(msg) zip_file = zipfile.ZipFile(io.BytesIO(response.content)) zip_file.extractall(con.UPD_DIR) done = False unzip_dir = str() for _, dirs, _ in os.walk(con.UPD_DIR): for d in dirs: unzip_dir = d done = True break if done: break self._update_bot(os.path.join(con.UPD_DIR, unzip_dir)) Cfg.set(new_hash, "update", "update_hash") msg = f"{emo.CHECK} Updating bot..." bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) if restart: msg = f"{emo.WAIT} Restarting bot..." update.message.reply_text(msg) time.sleep(0.2) os.execl(sys.executable, sys.executable, *sys.argv) else: msg = "Bot /restart needed" update.message.reply_text(msg)
def get_action(self, bot, update, args): if args: command = args[0].lower() # Execute raw SQL if command == "sql": if Cfg.get("database", "use_db"): args.pop(0) sql = " ".join(args) data = self.tgb.db.execute_sql(sql) if data["error"]: msg = data["error"] elif data["result"]: msg = '\n'.join(str(s) for s in data["result"]) else: msg = f"{emo.INFO} No data returned" update.message.reply_text(msg) else: update.message.reply_text( f"{emo.INFO} Database not enabled") # Change configuration elif command == "cfg": args.pop(0) v = args[-1] v = v.lower() args.pop(-1) # Convert to boolean if v == "true" or v == "false": v = utl.str2bool(v) # Convert to integer elif v.isnumeric(): v = int(v) # Convert to null elif v == "null" or v == "none": v = None try: Cfg.set(v, *args) except Exception as e: return self.handle_error(e, update) update.message.reply_text("Config changed") # Send global message elif command == "msg": if Cfg.get("database", "use_db"): args.pop(0) sql = self.get_sql("global_msg") data = self.tgb.db.execute_sql(sql) title = "This is a global message to " \ "every user of @OpenCryptoBot:\n\n" msg = " ".join(args) for user_id in data or []: try: bot.send_message(chat_id=user_id[0], text=f"{title}{msg}") except Exception as e: self.handle_error(e, update, send_error=False) else: update.message.reply_text( f"{emo.INFO} Database not enabled") # Manage plugins elif command == "plg": args.pop(0) # LOAD plugin if args[0].lower() == "load": self.tgb.reload_plugin(args[1]) update.message.reply_text("Plugin loaded") # UNLOAD plugin elif args[0].lower() == "unload": self.tgb.remove_plugin(args[1]) update.message.reply_text("Plugin unloaded") else: usr = update.effective_user.first_name update.message.reply_text( text=f"Welcome {usr}.\nChoose a statistic", reply_markup=self._keyboard_stats())
def get_action(self, bot, update, args): # Check if database is enabled if not Cfg.get("database", "use_db"): update.message.reply_text( text=f"{emo.ERROR} Plugin '{type(self).__name__}' " f"can't be used since database is disabled", parse_mode=ParseMode.MARKDOWN) return # Check if any arguments provided if not args: update.message.reply_text(text=f"Usage:\n{self.get_usage()}", parse_mode=ParseMode.MARKDOWN) return # 'list' argument - show all repeaters for a user if args[0].lower() == "list": chat_id = update.message.chat.id user_id = update.message.from_user.id repeaters = self.tgb.db.read_rep(user_id, chat_id) if repeaters: for rep in repeaters: chat = self.tgb.db.read_chat(rep[2]) chat_name = chat[2] if chat else None update.message.reply_text( text=f"Command:\n`{rep[3]}`\n" f"Chat:\n`{chat_name}`\n\n" f"↺ {rep[4]} seconds\n\n" f"(ID: {rep[0]})", parse_mode=ParseMode.MARKDOWN, reply_markup=self._keyboard_remove_rep()) return else: update.message.reply_text(f"{emo.INFO} No repeaters active") return # Extract time interval interval = str() if args[0].startswith("i="): interval = args[0].replace("i=", "") args.pop(0) # Check if time interval is provided if not interval: update.message.reply_text( text=f"{emo.ERROR} Time interval has to be provided", parse_mode=ParseMode.MARKDOWN) return # In seconds interval = utl.get_seconds(interval) # Check if interval was successfully converted to seconds if not interval: update.message.reply_text( text=f"{emo.ERROR} Wrong format for time interval", parse_mode=ParseMode.MARKDOWN) return # Check for command to repeat if not args: update.message.reply_text( text=f"{emo.ERROR} Provide command to repeat", parse_mode=ParseMode.MARKDOWN) return # Check if command is repeater itself if args[0].replace("/", "") in self.get_cmds(): update.message.reply_text( text=f"{emo.ERROR} Repeater can't repeat itself", parse_mode=ParseMode.MARKDOWN) return # Set command to repeat as current message text update.message.text = " ".join(args) try: self._run_repeater(update, interval) self.tgb.db.save_rep(update, interval) except IntegrityError as ie: err = "Repeater already saved" update.message.reply_text(f"{emo.ERROR} {err}") logging.warning(f"{err} {ie}") return except Exception as e: update.message.reply_text(f"{emo.ERROR} {e}") raise e update.message.reply_text(text=f"{emo.CHECK} Timer is active")