Example #1
0
    def __init__(self, tg_bot):
        self._tgb = tg_bot

        # Create access to global config
        self.global_config = self._tgb.config

        # Create access to plugin config
        cfg_path = os.path.join(self.get_cfg_path(), f"{self.get_name()}.json")
        self.config = ConfigManager(cfg_path)
Example #2
0
    def __init__(self, config: ConfigManager, token):
        self.config = config

        read_timeout = self.config.get("telegram", "read_timeout")
        connect_timeout = self.config.get("telegram", "connect_timeout")

        tgb_kwargs = dict()

        if read_timeout:
            tgb_kwargs["read_timeout"] = read_timeout
        if connect_timeout:
            tgb_kwargs["connect_timeout"] = connect_timeout

        try:
            self.updater = Updater(token, request_kwargs=tgb_kwargs)
        except InvalidToken as e:
            logging.error(e)
            exit("ERROR: Bot token not valid")

        self.job_queue = self.updater.job_queue
        self.dispatcher = self.updater.dispatcher

        host = self.config.get("idena", "host")
        port = self.config.get("idena", "port")
        timeout = self.config.get("idena", "timeout")
        api_key = self.config.get("idena", "api_key")
        self.api = IdenaAPI(host, port, timeout, api_key)

        try:
            node_version = self.api.node_version()
            if "error" in node_version:
                msg = f"{emo.ERROR} Couldn't connect to IDENA node on host {host} and port {port}"
                raise Exception(f"{msg} - Result: {node_version}")
            else:
                msg = f"{emo.CHECK} Successfully connected to IDENA node version {node_version['result']}"
                logging.info(msg)
        except Exception as e:
            logging.error(e)
            raise SystemExit

        # Load classes in folder 'plugins'
        self._load_plugins()

        # Handler for file downloads (plugin updates)
        mh = MessageHandler(Filters.document, self._update_plugin)
        self.dispatcher.add_handler(mh)

        # Handle all Telegram related errors
        self.dispatcher.add_error_handler(self._handle_tg_errors)

        # Send message to admin
        for admin in config.get("admin", "ids"):
            try:
                self.updater.bot.send_message(
                    admin, f"{emo.DONE} Bot is up and running!")
            except Exception as e:
                logging.warning(
                    f"Couldn't send startup message to ID {admin}: {e}")
Example #3
0
    def __init__(self):
        # Parse command line arguments
        self.args = self._parse_args()

        # Set up logging
        self._init_logger()

        # Read global config file and create Telegram bot
        self.cfg = Cfg(os.path.join(con.DIR_CFG, con.FILE_CFG))
        self.tgb = TelegramBot(self.cfg, self._get_bot_token())
Example #4
0
    def __init__(self, config: ConfigManager, token):
        self.config = config

        read_timeout = self.config.get("telegram", "read_timeout")
        connect_timeout = self.config.get("telegram", "connect_timeout")

        tgb_kwargs = dict()

        if read_timeout:
            tgb_kwargs["read_timeout"] = read_timeout
        if connect_timeout:
            tgb_kwargs["connect_timeout"] = connect_timeout

        try:
            self.updater = Updater(token, request_kwargs=tgb_kwargs)
        except InvalidToken as e:
            logging.error(e)
            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 file downloads (plugin updates)
        mh = MessageHandler(Filters.document, self._update_plugin)
        self.dispatcher.add_handler(mh)

        # Handle all Telegram related errors
        self.dispatcher.add_error_handler(self._handle_tg_errors)

        # Send message to admin
        for admin in config.get("admin", "ids"):
            try:
                self.updater.bot.send_message(
                    admin, f"{emo.DONE} Bot is up and running!")
            except Exception as e:
                logging.warning(
                    f"Couldn't send startup message to ID {admin}: {e}")
Example #5
0
class Idena:
    def __init__(self):
        # Parse command line arguments
        self.args = self._parse_args()

        # Set up logging
        self._init_logger()

        # Read global config file and create Telegram bot
        self.cfg = Cfg(os.path.join(con.DIR_CFG, con.FILE_CFG))
        self.tgb = TelegramBot(self.cfg, self._get_bot_token())

    def _parse_args(self):
        """ Parse command line arguments """
        desc = "Telegram Tron Bot For Betting"
        parser = ArgumentParser(description=desc)

        # Save logfile
        parser.add_argument("--no-log",
                            dest="savelog",
                            action="store_false",
                            help="don't save log-files",
                            required=False,
                            default=True)

        # Log level
        parser.add_argument(
            "-log",
            dest="loglevel",
            type=int,
            choices=[0, 10, 20, 30, 40, 50],
            help="disabled, debug, info, warning, error, critical",
            default=30,
            required=False)

        # Module log level
        parser.add_argument("-mlog",
                            dest="mloglevel",
                            help="set log level for a module",
                            default=None,
                            required=False)

        # Bot token
        parser.add_argument("-tkn",
                            dest="token",
                            help="set Telegram bot token",
                            required=False,
                            default=None)

        # Bot token via input
        parser.add_argument("--input-tkn",
                            dest="input_token",
                            action="store_true",
                            help="set Telegram bot token",
                            required=False,
                            default=False)

        return parser.parse_args()

    # Configure logging
    def _init_logger(self):
        """ Initialize the console logger and file logger """
        logger = logging.getLogger()
        logger.setLevel(self.args.loglevel)

        log_file = os.path.join(con.DIR_LOG, con.FILE_LOG)
        log_format = "[%(asctime)s %(levelname)s %(filename)s:%(lineno)s %(funcName)s()] %(message)s"

        # Log to console
        console_log = logging.StreamHandler()
        console_log.setFormatter(logging.Formatter(log_format))
        console_log.setLevel(self.args.loglevel)

        logger.addHandler(console_log)

        # Save logs if enabled
        if self.args.savelog:
            # Create 'log' directory if not present
            log_path = os.path.dirname(log_file)
            if not os.path.exists(log_path):
                os.makedirs(log_path)

            file_log = TimedRotatingFileHandler(log_file,
                                                when="H",
                                                encoding="utf-8")

            file_log.setFormatter(logging.Formatter(log_format))
            file_log.setLevel(self.args.loglevel)

            logger.addHandler(file_log)

        # Set log level for specified modules
        if self.args.mloglevel:
            for modlvl in self.args.mloglevel.split(","):
                module, loglvl = modlvl.split("=")
                logr = logging.getLogger(module)
                logr.setLevel(int(loglvl))

    # Read bot token from file
    def _get_bot_token(self):
        """ Read Telegram bot token from config file or command line or input """
        if self.args.input_token:
            return input("Enter Telegram Bot Token: ")
        if self.args.token:
            return self.args.token

        token_path = os.path.join(con.DIR_CFG, con.FILE_TKN)

        try:
            if os.path.isfile(token_path):
                with open(token_path, "r", encoding="utf8") as file:
                    return json.load(file)["telegram"]
            else:
                exit(
                    f"ERROR: No token file '{con.FILE_TKN}' found at '{token_path}'"
                )
        except KeyError as e:
            cls_name = f"Class: {type(self).__name__}"
            logging.error(f"{repr(e)} - {cls_name}")
            exit("ERROR: Can't read bot token")

    def start(self):
        if self.cfg.get("webhook", "use_webhook"):
            self.tgb.bot_start_webhook()
        else:
            self.tgb.bot_start_polling()

        self.tgb.bot_idle()
Example #6
0
    def execute(self, bot, update, args):
        if not args:
            update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                      parse_mode=ParseMode.MARKDOWN)
            return

        command = args[0].lower()
        args.pop(0)

        plugin = args[0].lower()
        args.pop(0)

        # ---- Change configuration ----
        if command == "cfg":
            conf = args[0].lower()
            args.pop(0)

            get_set = args[0].lower()
            args.pop(0)

            # SET a config value
            if get_set == "set":
                # Get value for key
                value = args[-1].replace("__", " ")
                args.pop(-1)

                # Check value for boolean
                if value.lower() == "true" or value.lower() == "false":
                    value = utl.str2bool(value)

                # Check value for integer
                elif value.isnumeric():
                    value = int(value)

                # Check value for null
                elif value.lower() == "null" or value.lower() == "none":
                    value = None

                try:
                    if plugin == "-":
                        value = self.global_config.set(value, *args)
                    else:
                        cfg_file = f"{conf}.json"
                        plg_conf = self.get_cfg_path(plugin=plugin)
                        cfg_path = os.path.join(plg_conf, cfg_file)
                        ConfigManager(cfg_path).set(value, *args)
                except Exception as e:
                    logging.error(e)
                    msg = f"{emo.ERROR} {e}"
                    update.message.reply_text(msg)
                    return

                update.message.reply_text(f"{emo.CHECK} Config changed")

            # GET a config value
            elif get_set == "get":
                try:
                    if plugin == "-":
                        value = self.global_config.get(*args)
                    else:
                        cfg_file = f"{conf}.json"
                        plg_conf = self.get_cfg_path(plugin=plugin)
                        cfg_path = os.path.join(plg_conf, cfg_file)
                        value = ConfigManager(cfg_path).get(*args)
                except Exception as e:
                    logging.error(e)
                    msg = f"{emo.ERROR} {e}"
                    update.message.reply_text(msg)
                    return

                update.message.reply_text(value)

            # Wrong syntax
            else:
                update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                          parse_mode=ParseMode.MARKDOWN)

        # ---- Manage plugins ----
        elif command == "plg":
            try:
                # Start plugin
                if args[0].lower() == "add":
                    res = self.add_plugin(plugin)

                # Stop plugin
                elif args[0].lower() == "remove":
                    res = self.remove_plugin(plugin)

                # Wrong sub-command
                else:
                    update.message.reply_text(
                        text="Only `add` and `remove` are supported",
                        parse_mode=ParseMode.MARKDOWN)
                    return

                # Reply with message
                if res["success"]:
                    update.message.reply_text(f"{emo.CHECK} {res['msg']}")
                else:
                    update.message.reply_text(f"{emo.ERROR} {res['msg']}")
            except Exception as e:
                update.message.reply_text(text=f"{emo.ERROR} {e}")

        else:
            update.message.reply_text(text=f"Unknown command `{command}`",
                                      parse_mode=ParseMode.MARKDOWN)
Example #7
0
class IdenaPlugin:
    def __init__(self, tg_bot):
        self._tgb = tg_bot

        # Create access to global config
        self.global_config = self._tgb.config

        # Create access to plugin config
        cfg_path = os.path.join(self.get_cfg_path(), f"{self.get_name()}.json")
        self.config = ConfigManager(cfg_path)

    def __enter__(self):
        """ This method gets executed before the plugin gets loaded.
        Make sure to return 'self' if you override it """
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """ This method gets executed after the plugin gets loaded """
        pass

    def execute(self, bot, update, args):
        """ Override this to be executed after command gets triggered """
        method = inspect.currentframe().f_code.co_name
        msg = f"Method '{method}' not implemented for plugin '{self.get_name()}'"
        logging.warning(msg)

    def get_usage(self):
        """ Return how to use the command """
        usage = self.get_resource(f"{self.get_name()}.md")

        if usage:
            usage = usage.replace("{{handle}}", self.get_handle())
            return usage

        return None

    def get_handle(self):
        """ Return the command string that triggers the plugin """
        return self.config.get("handle")

    def get_category(self):
        """ Return the category of the plugin for the 'help' command """
        return self.config.get("category")

    def get_description(self):
        """ Return the description of the plugin """
        return self.config.get("description")

    def get_plugins(self):
        """ Return a list of all active plugins """
        return self._tgb.plugins

    def get_jobs(self):
        """ Return a tuple with all currently active jobs """
        return self._tgb.job_queue.jobs()

    def get_job(self, name=None):
        """ Return the periodic job with the given name or
        None if 'interval' is not set in plugin config """

        name = self.get_name() if not name else name
        jobs = self._tgb.job_queue.get_jobs_by_name(name)

        if not jobs or len(jobs) < 1:
            return None

        return jobs[0]

    # TODO: Don't set default for name as plugin name. What if we have more than one?
    def repeat_job(self, callback, interval, first=0, context=None):
        """ Logic that gets executed periodically """
        self._tgb.job_queue.run_repeating(callback,
                                          interval,
                                          first=first,
                                          name=self.get_name(),
                                          context=context)

    def add_handler(self, handler, group=0):
        self._tgb.dispatcher.add_handler(handler, group=group)

    def add_plugin(self, module_name):
        """ Enable a plugin """
        return self._tgb.add_plugin(module_name)

    def remove_plugin(self, module_name):
        """ Disable a plugin """
        return self._tgb.remove_plugin(module_name)

    def api(self) -> IdenaAPI:
        return self._tgb.api

    def get_global_resource(self, filename):
        """ Return the content of the given file
        from the global 'resource' directory """

        path = os.path.join(os.getcwd(), c.DIR_RES, filename)

        try:
            with open(path, "r", encoding="utf8") as f:
                return f.read()
        except Exception as e:
            logging.error(e)
            self.notify(e)
            return None

    # TODO: Maybe give the option to only check filename without extension
    def get_resource(self, filename, plugin=""):
        """ Return the content of the given file from
        the 'resource' directory of the plugin """
        path = os.path.join(self.get_res_path(plugin), filename)

        try:
            with open(path, "r", encoding="utf8") as f:
                return f.read()
        except Exception as e:
            logging.error(e)
            self.notify(e)
            return None

    def execute_global_sql(self, sql, *args):
        """ Execute raw SQL statement on the global
        database and return the result if there is one """

        res = {"success": None, "data": None}

        # Check if database usage is enabled
        if not self.global_config.get("database", "use_db"):
            res["data"] = "Database disabled"
            res["success"] = False
            return res

        db_path = os.path.join(os.getcwd(), c.DIR_DAT, c.FILE_DAT)

        try:
            # Create directory if it doesn't exist
            directory = os.path.dirname(db_path)
            os.makedirs(directory, exist_ok=True)
        except Exception as e:
            res["data"] = str(e)
            res["success"] = False
            logging.error(e)
            self.notify(e)

        con = None

        try:
            con = sqlite3.connect(db_path)
            cur = con.cursor()
            cur.execute(sql, args)
            con.commit()

            res["data"] = cur.fetchall()
            res["success"] = True
        except Exception as e:
            res["data"] = str(e)
            res["success"] = False
            logging.error(e)
            self.notify(e)
        finally:
            if con:
                con.close()

            return res

    # TODO: Describe how arguments can be used
    def execute_sql(self, sql, *args, plugin="", db_name=""):
        """ Execute raw SQL statement on database for given
        plugin and return the result if there is one """

        res = {"success": None, "data": None}

        # Check if database usage is enabled
        if not self.global_config.get("database", "use_db"):
            res["data"] = "Database disabled"
            res["success"] = False
            return res

        if db_name:
            if not db_name.lower().endswith(".db"):
                db_name += ".db"
        else:
            if plugin:
                db_name = plugin + ".db"
            else:
                db_name = self.get_name() + ".db"

        if plugin:
            plugin = plugin.lower()
            data_path = self.get_dat_path(plugin=plugin)
            db_path = os.path.join(data_path, db_name)
        else:
            db_path = os.path.join(self.get_dat_path(), db_name)

        try:
            # Create directory if it doesn't exist
            directory = os.path.dirname(db_path)
            os.makedirs(directory, exist_ok=True)
        except Exception as e:
            res["data"] = str(e)
            res["success"] = False
            logging.error(e)
            self.notify(e)

        con = None

        try:
            con = sqlite3.connect(db_path)
            cur = con.cursor()
            cur.execute(sql, args)
            con.commit()

            res["data"] = cur.fetchall()
            res["success"] = True
        except Exception as e:
            res["data"] = str(e)
            res["success"] = False
            logging.error(e)
            self.notify(e)
        finally:
            if con:
                con.close()

            return res

    def global_table_exists(self, table_name):
        """ Return TRUE if given table exists in global database, otherwise FALSE """
        db_path = os.path.join(os.getcwd(), c.DIR_DAT, c.FILE_DAT)

        if not Path(db_path).is_file():
            return False

        con = sqlite3.connect(db_path)
        cur = con.cursor()
        exists = False

        statement = self.get_global_resource("table_exists.sql")

        try:
            if cur.execute(statement, [table_name]).fetchone():
                exists = True
        except Exception as e:
            logging.error(e)
            self.notify(e)

        con.close()
        return exists

    def table_exists(self, table_name, plugin="", db_name=""):
        """ Return TRUE if given table exists, otherwise FALSE """
        if db_name:
            if not db_name.lower().endswith(".db"):
                db_name += ".db"
        else:
            if plugin:
                db_name = plugin + ".db"
            else:
                db_name = self.get_name() + ".db"

        if plugin:
            db_path = os.path.join(self.get_dat_path(plugin=plugin), db_name)
        else:
            db_path = os.path.join(self.get_dat_path(), db_name)

        if not Path(db_path).is_file():
            return False

        con = sqlite3.connect(db_path)
        cur = con.cursor()
        exists = False

        statement = self.get_global_resource("table_exists.sql")

        try:
            if cur.execute(statement, [table_name]).fetchone():
                exists = True
        except Exception as e:
            logging.error(e)
            self.notify(e)

        con.close()
        return exists

    def get_name(self):
        """ Return the name of the current plugin """
        return type(self).__name__.lower()

    def get_res_path(self, plugin=""):
        """ Return path of resource directory for this plugin """
        if not plugin:
            plugin = self.get_name()
        return os.path.join(c.DIR_SRC, c.DIR_PLG, plugin, c.DIR_RES)

    def get_cfg_path(self, plugin=""):
        """ Return path of configuration directory for this plugin """
        if not plugin:
            plugin = self.get_name()
        return os.path.join(c.DIR_SRC, c.DIR_PLG, plugin, c.DIR_CFG)

    def get_dat_path(self, plugin=""):
        """ Return path of data directory for this plugin """
        if not plugin:
            plugin = self.get_name()
        return os.path.join(c.DIR_SRC, c.DIR_PLG, plugin, c.DIR_DAT)

    def get_plg_path(self, plugin=""):
        """ Return path of current plugin directory """
        if not plugin:
            plugin = self.get_name()
        return os.path.join(c.DIR_SRC, c.DIR_PLG, plugin)

    def plugin_available(self, plugin_name):
        """ Return TRUE if the given plugin is enabled or FALSE otherwise """
        for plugin in self.get_plugins():
            if plugin.get_name() == plugin_name.lower():
                return True
        return False

    def notify(self, some_input):
        """ All admins in global config will get a message with the given text.
         Primarily used for exceptions but can be used with other inputs too. """

        if self.global_config.get("admin", "notify_on_error"):
            for admin in self.global_config.get("admin", "ids"):
                try:
                    msg = f"{emo.ALERT} Admin Notification:\n{some_input}"
                    self._tgb.updater.bot.send_message(admin, msg)
                except Exception as e:
                    error = f"Not possible to notify admin id '{admin}'"
                    logging.error(f"{error}: {e}")
        return some_input

    @staticmethod
    def threaded(fn):
        """ Decorator for methods that have to run in their own thread """
        def _threaded(*args, **kwargs):
            thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
            thread.start()

            return thread

        return _threaded

    @classmethod
    def private(cls, func):
        """ Decorator for methods that need to be run in a private chat with the bot """
        def _private(self, bot, update, **kwargs):
            if self.config.get("private"):
                if bot.get_chat(update.message.chat_id).type == Chat.PRIVATE:
                    return func(self, bot, update, **kwargs)

        return _private

    @classmethod
    def send_typing(cls, func):
        """ Decorator for sending typing notification in the Telegram chat """
        def _send_typing(self, bot, update, **kwargs):
            if update.message:
                user_id = update.message.chat_id
            elif update.callback_query:
                user_id = update.callback_query.message.chat_id
            else:
                return func(self, bot, update, **kwargs)

            try:
                bot.send_chat_action(chat_id=user_id, action=ChatAction.TYPING)
            except Exception as e:
                logging.error(f"{e} - {update}")

            return func(self, bot, update, **kwargs)

        return _send_typing

    @classmethod
    def owner(cls, func):
        """
        Decorator that executes the method only if the user is an bot admin.

        The user ID that triggered the command has to be in the ["admin"]["ids"]
        list of the global config file 'config.json' or in the ["admins"] list
        of the currently used plugin config file.
        """
        def _owner(self, bot, update, **kwargs):
            user_id = update.effective_user.id

            admins_global = self.global_config.get("admin", "ids")
            if admins_global and isinstance(admins_global, list):
                if user_id in admins_global:
                    return func(self, bot, update, **kwargs)

            admins_plugin = self.config.get("admins")
            if admins_plugin and isinstance(admins_plugin, list):
                if user_id in admins_plugin:
                    return func(self, bot, update, **kwargs)

        return _owner

    @classmethod
    def dependency(cls, func):
        """ Decorator that executes a method only if the mentioned
        plugins in the config file of the current plugin are enabled """
        def _dependency(self, bot, update, **kwargs):
            dependencies = self.config.get("dependency")

            if dependencies and isinstance(dependencies, list):
                plugins = [p.get_name() for p in self.get_plugins()]

                for dependency in dependencies:
                    if dependency.lower() not in plugins:
                        return

            return func(self, bot, update, **kwargs)

        return _dependency