Exemple #1
0
 def __init__(self, name: str, config: Config, secret_config: SecretConfig,
              intents: discord.Intents) -> None:
     super().__init__(intents=intents)
     self.repl = None
     bc.instance_name = self.instance_name = name
     self.config = config
     self.secret_config = secret_config
     self.bot_cache = BotCache(True)
     self.loop.create_task(self._process_reminders())
     self.loop.create_task(VoiceRoutine(self.bot_cache).start())
     self.loop.create_task(self._repl_routine())
     bc.config = self.config
     bc.commands = self.config.commands
     bc.background_loop = self.loop
     bc.latency = lambda: self.latency
     bc.change_status = self._change_status
     bc.change_presence = self.change_presence
     bc.close = self.close
     bc.secret_config = self.secret_config
     bc.info = BotInfo()
     bc.plugin_manager.register()
     bc.fetch_channel = self.fetch_channel
     if not bc.args.fast_start:
         log.debug("Started Markov model checks...")
         if bc.markov.check():
             log.info("Markov model has passed all checks")
         else:
             log.info(
                 "Markov model has not passed checks, but all errors were fixed"
             )
Exemple #2
0
    def stop(self, _, main_bot=True):
        if not BotCache(main_bot).exists():
            return log.error(
                "Could not stop the bot (cache file does not exist)")
        bot_cache = BotCache(main_bot).parse()
        pid = bot_cache["pid"]
        if pid is None:
            return log.error(
                "Could not stop the bot (cache file does not contain pid)")
        if psutil.pid_exists(pid):
            if sys.platform == "win32":
                # Reference to the original solution:
                # https://stackoverflow.com/a/64357453
                import ctypes

                kernel = ctypes.windll.kernel32
                kernel.FreeConsole()
                kernel.AttachConsole(pid)
                kernel.SetConsoleCtrlHandler(None, 1)
                kernel.GenerateConsoleCtrlEvent(0, 0)
            else:
                os.kill(pid, signal.SIGINT)
            while psutil.pid_exists(pid):
                log.debug("Bot is still running. Please, wait...")
                time.sleep(0.5)
            log.info("Bot is stopped!")
        else:
            log.error("Could not stop the bot (bot is not running)")
            BotCache(main_bot).remove()
Exemple #3
0
 async def process_subcommands(content, message, user, safe=False):
     command_indicators = {
         ')': '(',
         ']': '[',
         '`': '`',
         '}': '{',
     }
     while True:
         updated = False
         for i in range(len(content)):
             if content[i] in command_indicators.keys():
                 for j in range(i - 1, 0, -1):
                     if content[j] == command_indicators[
                             content[i]] and content[j - 1] == '$':
                         updated = True
                         message.content = content[j + 1:i]
                         command = message.content.split()
                         if not command:
                             return
                         if command[0] not in bc.config.commands.data.keys(
                         ):
                             if command[
                                     0] in bc.config.commands.aliases.keys(
                                     ):
                                 command[0] = bc.config.commands.aliases[
                                     command[0]]
                             else:
                                 await message.channel.send(
                                     f"Unknown command '{command[0]}'")
                         result = ""
                         if command and command[0] in bc.commands.data.keys(
                         ):
                             log.debug(
                                 f"Processing subcommand: {command[0]}: {message.content}"
                             )
                             cmd = bc.commands.data[command[0]]
                             if cmd.can_be_subcommand():
                                 result = await cmd.run(message,
                                                        command,
                                                        user,
                                                        silent=True)
                                 if result is None or (
                                         safe and not const.
                                         ALNUM_STRING_REGEX.match(content)):
                                     result = ""
                             else:
                                 await message.channel.send(
                                     f"Command '{command[0]}' can not be used as subcommand"
                                 )
                         content = content[:j - 1] + result + content[i +
                                                                      1:]
                         log.debug2(
                             f"Command (during processing subcommands): {content}"
                         )
                         break
             if updated:
                 break
         if not updated:
             break
     return content
Exemple #4
0
 def register(self, reload: bool = False) -> None:
     """Find plugins in plugins directory and register them"""
     plugin_directory = os.path.join(os.path.dirname(__file__), "plugins")
     plugin_modules = ['src.plugins.' + os.path.splitext(path)[0] for path in os.listdir(plugin_directory)
                       if os.path.isfile(os.path.join(plugin_directory, path)) and path.endswith(".py")]
     private_plugin_directory = os.path.join(os.path.dirname(__file__), "plugins", "private")
     plugin_modules += [Util.path_to_module(
         f"src.plugins.private.{os.path.relpath(path, private_plugin_directory)}."
         f"{os.path.splitext(file)[0]}")
         for path, _, files in os.walk(private_plugin_directory) for file in files
         if os.path.isfile(os.path.join(private_plugin_directory, path, file)) and file.endswith(".py")]
     importlib.invalidate_caches()
     for module in plugin_modules:
         log.debug2(f"Processing plugins from module: {module}")
         plugins_file = importlib.import_module(module)
         if reload:
             importlib.reload(plugins_file)
         plugins = [obj[1] for obj in inspect.getmembers(plugins_file, inspect.isclass)
                    if (obj[1].__module__ == module) and issubclass(obj[1], BasePlugin)]
         if len(plugins) == 1:
             plugin = plugins[0]
             actual_functions_list = [
                 func[0] for func in inspect.getmembers(plugin, inspect.isfunction)
                 if not func[0].startswith('_')
             ]
             if all(x in actual_functions_list for x in self._plugin_functions_interface):
                 p = plugin()
                 self._plugins[p.get_classname()] = p
                 log.debug(f"Registered plugin '{p.get_classname()}'")
             else:
                 log.error(f"Class '{p.get_classname()}' does comply with BasePlugin interface")
         elif len(plugins) > 1:
             log.error(f"Module '{module}' have more than 1 class in it")
         else:
             log.error(f"Module '{module}' have no classes in it")
Exemple #5
0
 async def _vqskip(message, command, silent=False):
     """Skip current track in voice queue
 Usage: !vqskip"""
     if not await Util.check_args_count(
             message, command, silent, min=1, max=1):
         return
     if bc.voice_client is not None:
         bc.voice_client.stop()
         await Msg.response(message, "Skipped current song", silent)
     else:
         log.debug("Nothing to skip")
Exemple #6
0
 def connect(self) -> None:
     self._db_client = pymongo.MongoClient(self.url,
                                           serverSelectionTimeoutMS=10,
                                           connectTimeoutMS=20000)
     try:
         info = self._db_client.server_info()
         log.debug(f"Mongo connection initialized: {info['version']}")
     except ServerSelectionTimeoutError as e:
         log.error(f"Mongo connection failed: {e}")
     self._db = self._db_client[self._db_name]
     self.markov = self._db["markov"]
Exemple #7
0
 async def run(self):
     command = self.message.content.split(' ')
     command = list(filter(None, command))
     command[0] = command[0][1:]
     while True:
         await asyncio.sleep(self.period)
         log.debug(f"Triggered background event: {' '.join(command)}")
         if command[0] not in self.config.commands.data.keys():
             await self.channel.send(f"Unknown command '{command[0]}'")
         else:
             cmd = self.config.commands.data[command[0]]
             saved_content = self.message.content
             await cmd.run(self.message, command, None)
             self.message.content = saved_content
Exemple #8
0
 async def _process_command(self, message: discord.Message) -> None:
     command = message.content.split(' ')
     command = list(filter(None, command))
     command[0] = command[0][1:]
     if not command[0]:
         return log.debug("Ignoring empty command")
     if command[0] not in self.config.commands.data.keys():
         if command[0] in self.config.commands.aliases.keys():
             command[0] = self.config.commands.aliases[command[0]]
         else:
             await message.channel.send(
                 f"Unknown command '{command[0]}', "
                 f"probably you meant '{self._suggest_similar_command(command[0])}'"
             )
             return
     if command[0] not in (
             "poll",
             "timer",
             "stopwatch",
             "vqpush",
     ):
         timeout_error, _ = await Util.run_function_with_time_limit(
             self.config.commands.data[command[0]].run(
                 message, command, self.config.users[message.author.id]),
             const.MAX_COMMAND_EXECUTION_TIME)
         if command[0] not in ("silent", ) and timeout_error:
             await message.channel.send(
                 f"Command '{' '.join(command)}' took too long to execute")
     else:
         await self.config.commands.data[command[0]].run(
             message, command, self.config.users[message.author.id])
Exemple #9
0
 async def send_direct_message(author, content, silent, **kwargs):
     """Send direct message to message author"""
     if silent:
         return log.debug("[SILENT] -> " + content)
     if author.dm_channel is None:
         await author.create_dm()
     msg = None
     if content:
         for chunk in Msg.split_by_chunks(content,
                                          const.DISCORD_MAX_MESSAGE_LENGTH):
             msg = await author.dm_channel.send(
                 chunk,
                 tts=kwargs.get("tts", False),
                 files=kwargs.get("files", None),
                 embed=kwargs.get("embed", None))
             if kwargs.get("suppress_embeds", False):
                 await msg.edit(suppress=True)
     elif kwargs.get("files", None):
         msg = await author.dm_channel.send(None,
                                            files=kwargs.get("files", None))
     elif kwargs.get("embed", None):
         msg = await author.dm_channel.send(embed=kwargs["embed"],
                                            tts=kwargs.get("tts", False))
         if kwargs.get("suppress_embeds", False):
             await msg.edit(suppress=True)
     return msg
Exemple #10
0
 def check_versions(self) -> bool:
     """Compare versions from updated version.py with current ones"""
     import src.version as ver
     importlib.reload(ver)
     updated = False
     if self.config_version != ver.CONFIG_VERSION:
         self.config_version = ver.CONFIG_VERSION
         updated = True
     if self.markov_version != ver.MARKOV_CONFIG_VERSION:
         self.markov_version = ver.MARKOV_CONFIG_VERSION
         updated = True
     if self.secret_version != ver.SECRET_CONFIG_VERSION:
         self.secret_version = ver.SECRET_CONFIG_VERSION
         updated = True
     log.debug(f"Config versions were{'' if updated else ' not'} updated")
     return updated
Exemple #11
0
def start(args) -> None:
    au = importlib.import_module("src.autoupdate_impl")
    signal.signal(signal.SIGHUP, au.at_exit)
    context = au.AutoUpdateContext()
    au.at_start()
    try:
        while True:
            time.sleep(const.AUTOUPDATE_CHECK_INTERVAL)
            is_updated = au.check_updates(context)
            if is_updated:
                au = importlib.reload(au)
                signal.signal(signal.SIGHUP, au.at_exit)
                log.debug("Reloaded autoupdate implementation module")
    except KeyboardInterrupt as e:
        au.at_failure(e)
    except Exception as e:
        au.at_exit()
        raise e
    au.at_exit()
Exemple #12
0
 async def run_external_command(message, cmd_line, silent=False):
     result = ""
     try:
         log.debug(f"Processing external command: '{cmd_line}'")
         process = subprocess.run(cmd_line,
                                  shell=True,
                                  check=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  env=os.environ)
         log.debug(
             f"External command '{cmd_line}' finished execution with return code: {process.returncode}"
         )
         result = process.stdout.decode("utf-8")
         await Msg.response(message, result, silent)
     except subprocess.CalledProcessError as e:
         await Msg.response(
             message, f"<Command failed with error code {e.returncode}>",
             silent)
     return result
Exemple #13
0
def check_updates(context: AutoUpdateContext) -> bool:
    """Function that performs updates check. It is called periodically"""
    old_sha = context.repo.head.object.hexsha
    try:
        context.repo.remotes.origin.fetch()
    except Exception as e:
        return log.error(
            f"Fetch failed: {e}. Skipping this cycle, will try to update on the next one"
        )
    new_sha = context.repo.remotes.origin.refs['master'].object.name_rev.split(
    )[0]
    log.debug(f"{old_sha} {new_sha}")
    if old_sha == new_sha:
        return log.debug("No new updates")
    bot_cache = importlib.import_module("src.bot_cache").BotCache(True).parse()
    if bot_cache is None:
        return log.warning(
            "Could not read bot cache. Skipping this cycle, will try to update on the next one"
        )
    if "do_not_update" not in bot_cache.keys():
        return log.warning(
            "Could not find 'do_not_update' field in bot cache. "
            "Skipping this cycle, will try to update on the next one")
    if bot_cache["do_not_update"]:
        return log.debug(
            "Automatic update is not permitted. Skipping this cycle, will try to update on the next one"
        )
    context.repo.git.reset("--hard")
    try:
        g = git.cmd.Git(os.getcwd())
        g.pull()
    except git.exc.GitCommandError as e:
        if "Connection timed out" in e.stderr or "Could not resolve host" in e.stderr:
            log.warning(f"{e.command}: {e.stderr}")
        else:
            raise e
    subprocess.call(f"{sys.executable} -m pip install -r requirements.txt",
                    shell=True)
    minibot_response = "WalBot automatic update is in progress. Please, wait..."
    subprocess.call(
        f"{sys.executable} walbot.py startmini --message '{minibot_response}' --nohup &",
        shell=True)
    subprocess.call(f"{sys.executable} walbot.py stop", shell=True)
    if context.check_versions():
        subprocess.call(f"{sys.executable} walbot.py patch", shell=True)
    subprocess.call(f"{sys.executable} walbot.py start --fast_start --nohup &",
                    shell=True)
    while True:
        time.sleep(1)
        bot_cache = importlib.import_module("src.bot_cache").BotCache(
            True).parse()
        if bot_cache is not None and bot_cache["ready"]:
            subprocess.call(f"{sys.executable} walbot.py stopmini", shell=True)
            log.info("Bot is fully loaded. MiniWalBot is stopped.")
            break
        log.debug("Bot is not fully loaded yet. Waiting...")
    return True
Exemple #14
0
 def get_yaml(verbose: bool = False) -> Any:
     """Get YAML loader and dumper type.
     yaml.Loader and yaml.Dumper are slower implementations than yaml.CLoader and yaml.CDumper"""
     try:
         loader = yaml.CLoader
         if verbose:
             log.debug("Using fast YAML Loader")
     except AttributeError:
         loader = yaml.Loader
         if verbose:
             log.debug("Using slow YAML Loader")
     try:
         dumper = yaml.CDumper
         if verbose:
             log.debug("Using fast YAML Dumper")
     except AttributeError:
         dumper = yaml.Dumper
         if verbose:
             log.debug("Using slow YAML Dumper")
     return loader, dumper
Exemple #15
0
 async def _vleave(message, command, silent=False):
     """Leave (part) voice channel
 Usage: !vleave"""
     if not await Util.check_args_count(
             message, command, silent, min=1, max=1):
         return
     if bc.voice_client is not None:
         log.debug("Leaving previous voice channel...")
         await bc.voice_client.disconnect()
         log.debug("Left previous voice channel")
         bc.voice_client = None
     else:
         log.debug("No previous voice channel to leave")
Exemple #16
0
 async def reply(message, content, silent, **kwargs):
     """Reply on particular message"""
     if silent:
         return log.debug("[SILENT] -> " + content)
     msg = None
     if content:
         for chunk in Msg.split_by_chunks(content,
                                          const.DISCORD_MAX_MESSAGE_LENGTH):
             msg = await message.reply(chunk,
                                       tts=kwargs.get("tts", False),
                                       files=kwargs.get("files", None),
                                       embed=kwargs.get("embed", None))
             if kwargs.get("suppress_embeds", False):
                 await msg.edit(suppress=True)
     elif kwargs.get("files", None):
         msg = await message.reply(None, files=kwargs.get("files", None))
     elif kwargs.get("embed", None):
         msg = await message.reply(embed=kwargs["embed"],
                                   tts=kwargs.get("tts", False))
         if kwargs.get("suppress_embeds", False):
             await msg.edit(suppress=True)
     return msg
Exemple #17
0
 async def _vjoin(message, command, silent=False):
     """Join voice channel
 Usage: !vjoin <voice_channel_id>"""
     if not await Util.check_args_count(
             message, command, silent, min=2, max=2):
         return
     voice_channel_id = await Util.parse_int(
         message, command[1],
         f"Second parameter for '{command[0]}' should be an id of voice channel",
         silent)
     if voice_channel_id is None:
         return
     voice_channels = message.guild.voice_channels
     for v in voice_channels:
         if v.id == voice_channel_id:
             if bc.voice_client is not None:
                 log.debug("Disconnecting from previous voice channel...")
                 await bc.voice_client.disconnect()
                 log.debug("Disconnected from previous voice channel")
                 bc.voice_client = None
             log.debug(
                 f"Connecting to the voice channel {voice_channel_id}...")
             try:
                 bc.voice_client = await v.connect()
             except Exception as e:
                 return null(await
                             Msg.response(message,
                                          f"ERROR: Failed to connect: {e}",
                                          silent))
             log.debug("Connected to the voice channel")
             break
     else:
         await Msg.response(
             message,
             f"🔊 Could not find voice channel with id {voice_channel_id}",
             silent)
Exemple #18
0
 async def push_video(message, yt_video_url, silent):
     """Push video by its URL to voice queue"""
     r = const.YT_VIDEO_REGEX.match(yt_video_url)
     if r is None:
         return
     yt_video_id = r.groups()[0]
     output_file_name = f'{Util.tmp_dir()}/yt_{yt_video_id}.mp3'
     ydl_opts = {
         'format':
         'bestaudio/best',
         'outtmpl':
         output_file_name,
         'postprocessors': [{
             'key': 'FFmpegExtractAudio',
             'preferredcodec': 'mp3',
         }],
     }
     try:
         with youtube_dl.YoutubeDL(ydl_opts) as ydl:
             video_info = ydl.extract_info(yt_video_url, download=False)
             if not os.path.exists(output_file_name):
                 log.debug(f"Downloading YT video {yt_video_url} ...")
                 loop = asyncio.get_event_loop()
                 await loop.run_in_executor(None, ydl.download,
                                            [yt_video_url])
                 log.debug(f"Downloaded {yt_video_url}")
             else:
                 log.debug(f"Found in cache: {yt_video_url}")
     except Exception as e:
         return null(await
                     Msg.response(message,
                                  f"🔊 ERROR: Downloading failed: {e}",
                                  silent))
     bc.voice_client_queue.append(
         VoiceQueueEntry(message.channel, video_info['title'],
                         video_info['id'], output_file_name,
                         message.author.name))
     await Msg.response(
         message,
         f"🔊 Added {video_info['title']} (YT: {video_info['id']}) to the queue "
         f"at position #{len(bc.voice_client_queue)}", silent)
Exemple #19
0
def at_start() -> None:
    """Autoupdate initialization"""
    if not os.path.isfile(const.BOT_CACHE_FILE_PATH):
        log.debug("Bot is not started! Starting...")
        subprocess.call(
            f"{sys.executable} walbot.py start --fast_start --nohup &",
            shell=True)
    else:
        bot_cache = BotCache(True).parse()
        pid = None
        if bot_cache is not None:
            pid = bot_cache["pid"]
        if pid is not None and psutil.pid_exists(pid):
            log.debug(
                "Bot is already started in different shell. Starting autoupdate routine."
            )
        else:
            if os.path.isfile(const.BOT_CACHE_FILE_PATH):
                os.remove(const.BOT_CACHE_FILE_PATH)
            log.debug("Bot is not started! Starting...")
            subprocess.call(
                f"{sys.executable} walbot.py start --fast_start --nohup &",
                shell=True)
Exemple #20
0
 async def _precompile(self) -> None:
     log.debug("Started precompiling functions...")
     levenshtein_distance("", "")
     log.debug("Finished precompiling functions")
Exemple #21
0
 async def run(self, message, command, user, silent=False):
     if len(inspect.stack(0)) >= const.MAX_SUBCOMMAND_DEPTH:
         return null(await message.channel.send(
             "ERROR: Maximum subcommand depth is reached!"))
     log.debug(f"Processing command: {message.content}")
     channel_id = message.channel.id
     if isinstance(
             message.channel,
             discord.Thread):  # Inherit command permissions for threads
         channel_id = message.channel.parent_id
     if not self.is_available(channel_id):
         return null(await message.channel.send(
             f"Command '{command[0]}' is not available in this channel"))
     if user is not None and self.permission > user.permission_level:
         return null(await message.channel.send(
             f"You don't have permission to call command '{command[0]}'"))
     self.times_called += 1
     postpone_execution = [
         "addcmd",
         "updcmd",
         "addextcmd",
         "updextcmd",
         "addbgevent",
         "addresponse",
         "updresponse",
     ]
     if message.content.split(' ')[0][1:] not in postpone_execution:
         log.debug2(f"Command (before processing): {message.content}")
         message.content = await self.process_subcommands(
             message.content, message, user)
         log.debug2(
             f"Command (after processing subcommands): {message.content}")
     else:
         log.debug2("Subcommands are not processed!")
     command = message.content[1:].split(' ')
     command = list(filter(None, command))
     if self.perform is not None:
         return await self.get_actor()(message, command, silent)
     elif self.message is not None:
         response = self.message
         log.debug2(f"Command (before processing): {response}")
         response = await self.process_variables(response, message, command)
         log.debug2(f"Command (after processing variables): {response}")
         response = await self.process_subcommands(response, message, user)
         log.debug2(f"Command (after processing subcommands): {response}")
         if response:
             if not silent:
                 if len(response) > const.DISCORD_MAX_MESSAGE_LENGTH * 5:
                     await message.channel.send(
                         "ERROR: Max message length exceeded "
                         f"({len(response)} > {const.DISCORD_MAX_MESSAGE_LENGTH * 5})"
                     )
                 elif len(const.UNICODE_EMOJI_REGEX.findall(response)) > 50:
                     await message.channel.send(
                         "ERROR: Max amount of Unicode emojis for one message exceeded "
                         f"({len(const.UNICODE_EMOJI_REGEX.findall(response))} > {50})"
                     )
                 else:
                     for chunk in Msg.split_by_chunks(
                             response, const.DISCORD_MAX_MESSAGE_LENGTH):
                         await message.channel.send(chunk)
             return response
     elif self.cmd_line is not None:
         cmd_line = self.cmd_line[:]
         log.debug2(f"Command (before processing): {cmd_line}")
         cmd_line = await self.process_variables(cmd_line,
                                                 message,
                                                 command,
                                                 safe=True)
         log.debug2(f"Command (after processing variables): {cmd_line}")
         cmd_line = await self.process_subcommands(cmd_line,
                                                   message,
                                                   user,
                                                   safe=True)
         log.debug2(f"Command (after processing subcommands): {cmd_line}")
         return await Util.run_external_command(message, cmd_line, silent)
     else:
         await message.channel.send(
             f"Command '{command[0]}' is not callable")