diff --git a/README.md b/README.md index b030cba..31c5266 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ using [HTTP API](https://gitea.brno.mraveniste.cc/turbotraktor/ladylilia.com/src * `!lastquote` - posts the most recent quote * `!findquote PATTERN` - searches for quotes matching `PATTERN` and in case multiple matches are found, posts one of them randomly - - `PATTERN` has to be at least 3 characters long and it can be enclosed in double quotes in case it contains spaces + - `PATTERN` has to be at least 3 characters long * `!bellagram`, `!bellapics`, `!instabella`, `!instagram` - posts a link to a random Instagram picture of Bella * `!yt QUERY` - queries Lady Lilia's Youtube channel and posts a link to the most relevant result - `QUERY` can contain `|` (logical **or**) and `-` (logical **not**) operators, for example: `!yt oblivion -nehrim` * `!clip PATTERN` - searches for Twitch clips matching `PATTERN` and in case multiple matches are found, posts one of them randomly - - `PATTERN` has to be at least 3 characters long and it can be enclosed in double quotes in case it contains spaces + - `PATTERN` has to be at least 3 characters long + +### Mod-only commands * `!syncquotes` - performs synchronization of the quotes database with Twitch VODs - - only master user is allowed to use this command -* `!reload` - reloads the list of extra commands from the config file - - only master user is allowed to use this command +* `!command set COMMAND TEXT` - adds a new extra command or updates an existing one +* `!command unset COMMAND` - removes an existing extra command diff --git a/bot.py b/bot.py index 878b8ec..e5ba9cc 100644 --- a/bot.py +++ b/bot.py @@ -4,6 +4,7 @@ import logging import os from commands import Commands +from extracommands import ExtraCommands from clients.discord import DiscordClient from clients.twitch import TwitchClient @@ -37,19 +38,29 @@ def main(): commands = Commands(config, commands_logger) + extra_commands = ExtraCommands() + + async def monitor_extra_commands(): + while True: + try: + extra_commands.reload_if_needed() + except Exception as e: + commands_logger.info('Exception', exc_info=e) + await asyncio.sleep(TIMEOUT) + async def run_twitch_client(): - twitch_client = TwitchClient(config, twitch_logger, commands) + twitch_client = TwitchClient(config, twitch_logger, commands, extra_commands) twitch_client.connect_() while True: try: twitch_client.process_data() - await asyncio.sleep(TIMEOUT) except Exception as e: twitch_logger.info('Exception', exc_info=e) + await asyncio.sleep(TIMEOUT) async def run_discord_client(): while True: - discord_client = DiscordClient(config, discord_logger, commands) + discord_client = DiscordClient(config, discord_logger, commands, extra_commands) try: await discord_client.start_() except Exception as e: @@ -58,6 +69,7 @@ def main(): loop = asyncio.get_event_loop() try: + asyncio.ensure_future(monitor_extra_commands()) asyncio.ensure_future(run_twitch_client()) asyncio.ensure_future(run_discord_client()) loop.run_forever() diff --git a/clients/discord.py b/clients/discord.py index 6da8a38..c09bbb5 100644 --- a/clients/discord.py +++ b/clients/discord.py @@ -31,10 +31,11 @@ def cooldown(retries, timeout, failure): class DiscordClient(discord.Client): - def __init__(self, config, logger, commands): + def __init__(self, config, logger, commands, extra_commands): self.config = config self.logger = logger self.commands = commands + self.extra_commands = extra_commands self.supported_commands = [ (re.compile(r'^!lastquote$'), self._do_lastquote), (re.compile(r'^!findquote\s+(?P")?(?P.+)(?(q)")$'), self._do_findquote), @@ -57,7 +58,7 @@ class DiscordClient(discord.Client): m = pattern.match(message.content) if m: await action(server, message.author.id, message, **m.groupdict()) - for cmd, resp in self.config['Extra Commands'].items(): + for cmd, resp in self.extra_commands.items(): if cmd == message.content: await self.send_message(message.channel, resp) diff --git a/clients/twitch.py b/clients/twitch.py index 7f12fae..49b4bd5 100644 --- a/clients/twitch.py +++ b/clients/twitch.py @@ -33,10 +33,11 @@ QUOTE_REMOVED_PATTERN = re.compile(r'''^ class TwitchClient(irc.bot.SingleServerIRCBot): - def __init__(self, config, logger, commands): + def __init__(self, config, logger, commands, extra_commands): self.config = config self.logger = logger self.commands = commands + self.extra_commands = extra_commands self.patterns = [ (QUOTE_ADDED_PATTERN, self._add_quote), (QUOTE_EDITED_PATTERN, self._edit_quote), @@ -49,7 +50,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot): (re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), (re.compile(r'^!yt\s+(?P")?(?P.+)(?(q)")$'), self._do_yt), (re.compile(r'^!clip\s+(?P")?(?P.+)(?(q)")$'), self._do_clip), - (re.compile(r'^!reload$'), self._do_reload), + (re.compile(r'^!command\s+set\s+(?P")?(?P.+?)(?(q1)")\s+' + r'(?P")?(?P.+)(?(q2)")$'), self._do_command_set), + (re.compile(r'^!command\s+unset\s+(?P")?(?P.+)(?(q)")$'), self._do_command_unset), ] server = self.config['IRC'].get('server') port = self.config['IRC'].getint('port') @@ -82,6 +85,10 @@ class TwitchClient(irc.bot.SingleServerIRCBot): def on_whisper(self, connection, event): self._process_message(connection, event) + def _is_mod(self, tags): + badges = [b.split('/')[0] for b in tags['badges'].split(',')] + return bool(set(badges).intersection(['admin', 'broadcaster', 'moderator'])) + def _send_response(self, connection, event, msg): if event.target.startswith('#'): connection.privmsg(event.target, msg) @@ -97,7 +104,7 @@ class TwitchClient(irc.bot.SingleServerIRCBot): m = pattern.match(message) if m: action(tags, send_response, **m.groupdict()) - for cmd, resp in self.config['Extra Commands'].items(): + for cmd, resp in self.extra_commands.items(): if cmd == message: send_response(resp) @@ -149,9 +156,8 @@ class TwitchClient(irc.bot.SingleServerIRCBot): send_response('One time, Lilia said this... #{id}: "{text}" [{game}] [{date}]'.format(**quote)) def _do_syncquotes(self, tags, send_response, **kwargs): - master_user_id = self.config['Twitch'].getint('master_user_id') - if int(tags['user-id']) != master_user_id: - respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) + if not self._is_mod(tags): + send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) return try: messages = self.commands.get_quote_messages() @@ -163,6 +169,7 @@ class TwitchClient(irc.bot.SingleServerIRCBot): m = pattern.match(message) if m: action(tags, send_response, **m.groupdict()) + send_response('Sync finished, @{0}'.format(tags['display-name'])) def _do_bellagram(self, tags, send_response, **kwargs): try: @@ -188,10 +195,28 @@ class TwitchClient(irc.bot.SingleServerIRCBot): else: send_response('{0}: {1}'.format(result['title'], result['url'])) - def _do_reload(self, tags, send_response, **kwargs): - master_user_id = self.config['Twitch'].getint('master_user_id') - if int(tags['user-id']) != master_user_id: - respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) + def _do_command_set(self, tags, send_response, cmd, resp, **kwargs): + if not self._is_mod(tags): + send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) return - self.config.remove_section('Extra Commands') - self.config.read('settings.cfg') + try: + self.extra_commands[cmd] = resp + self.extra_commands.save() + except Exception as e: + self.logger.error('Failed to update extra commands: %s', e) + send_response('Sorry @{0}, failed to set the command'.format(tags['display-name'])) + else: + send_response('Command {0} set, @{1}'.format(cmd, tags['display-name'])) + + def _do_command_unset(self, tags, send_response, cmd, **kwargs): + if not self._is_mod(tags): + send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) + return + try: + self.extra_commands.pop(cmd, None) + self.extra_commands.save() + except Exception as e: + self.logger.error('Failed to update extra commands: %s', e) + send_response('Sorry @{0}, failed to unset the command'.format(tags['display-name'])) + else: + send_response('Command {0} unset, @{1}'.format(cmd, tags['display-name'])) diff --git a/docker-compose.yaml.example b/docker-compose.yaml.example index e38e2e8..a7b9e3a 100644 --- a/docker-compose.yaml.example +++ b/docker-compose.yaml.example @@ -6,5 +6,6 @@ services: context: . volumes: - /bot/settings.cfg:/bot/settings.cfg + - /bot/extra-commands.json:/bot/extra-commands.json environment: - DEBUG=0 diff --git a/extracommands.py b/extracommands.py new file mode 100644 index 0000000..ddd4975 --- /dev/null +++ b/extracommands.py @@ -0,0 +1,27 @@ +import json +import os + + +FILENAME = 'extra-commands.json' + + +class ExtraCommands(dict): + + def __init__(self): + self.last_mtime = 0 + + def reload(self): + with open(FILENAME) as f: + commands = json.load(f) + self.clear() + self.update(commands) + + def reload_if_needed(self): + mtime = os.stat(FILENAME).st_mtime + if mtime > self.last_mtime: + self.last_mtime = mtime + self.reload() + + def save(self): + with open(FILENAME, 'w') as f: + json.dump(self, f, indent=4, sort_keys=True) diff --git a/settings.cfg.example b/settings.cfg.example index f89269a..360ba25 100644 --- a/settings.cfg.example +++ b/settings.cfg.example @@ -10,8 +10,6 @@ token = __DISCORD_TOKEN__ [Twitch] cache_api_url = https://ladylilia.com/twitch-cache/api token = oauth:__TWITCH_OAUTH_TOKEN__ -# nikola_f -master_user_id = 210957066 [Instagram] username = lilialovesgames @@ -26,6 +24,3 @@ channel_ids = UC5970RJMoEcRNZl0MNp8tlQ,UCHNjavmkFUf2n0uzWSgj16w api_url = https://ladylilia.com/quotes/api api_key = __QUOTES_API_KEY__ act_as_proxy = true - -[Extra Commands] -;!command = Response Text