From 008d54016a9a60fe9e8a20c7a1d8dc6ec823fc26 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 16 Jul 2018 18:35:59 +0000 Subject: Add the BigBrother cog. --- bot/__main__.py | 1 + bot/cogs/bigbrother.py | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ config-default.yml | 3 + 3 files changed, 165 insertions(+) create mode 100644 bot/cogs/bigbrother.py diff --git a/bot/__main__.py b/bot/__main__.py index f470a42d6..9014bce14 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -51,6 +51,7 @@ bot.load_extension("bot.cogs.security") bot.load_extension("bot.cogs.events") # Commands, etc +bot.load_extension("bot.cogs.bigbrother") bot.load_extension("bot.cogs.bot") bot.load_extension("bot.cogs.cogs") diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py new file mode 100644 index 000000000..664f1b74c --- /dev/null +++ b/bot/cogs/bigbrother.py @@ -0,0 +1,161 @@ +import logging +from typing import List + +from discord import Color, Embed, Message, TextChannel, User +from discord.ext.commands import Bot, Context, command + +from bot.constants import Channels, Emojis, Keys, Roles, URLs +from bot.decorators import with_role +from bot.pagination import LinePaginator + + +log = logging.getLogger(__name__) + + +class BigBrother: + """User monitoring to assist with moderation.""" + + HEADERS = {'X-API-Key': Keys.site_api} + + def __init__(self, bot: Bot): + self.bot = bot + self.watched_users = {} + + def update_cache(self, api_response: List[dict]): + """ + Updates the internal cache of watched users from the given `api_response`. + This function will only add (or update) existing keys, it will not delete + keys that were not present in the API response. + A user is only added if the bot can find a channel + with the given `channel_id` in its channel cache. + """ + + for entry in api_response: + user_id = int(entry['user_id']) + channel_id = int(entry['channel_id']) + channel = self.bot.get_channel(channel_id) + + if channel is not None: + self.watched_users[user_id] = channel + else: + log.error( + f"Site specified to relay messages by `{user_id}` in `{channel_id}`, " + "but the given channel could not be found. Ignoring." + ) + + async def on_ready(self): + async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: + data = await response.json() + self.update_cache(data) + + async def on_message(self, msg: Message): + if msg.author.id in self.watched_users: + channel = self.watched_users[msg.author.id] + relay_content = (f"{Emojis.lemoneye2} {msg.author} sent the following " + f"in {msg.channel.mention}: {msg.clean_content}") + if msg.attachments: + relay_content += f" (with {len(msg.attachments)} attachment(s))" + + await channel.send(relay_content) + + @command(name='bigbrother.watched()', aliases=('bigbrother.watched',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watched_command(self, ctx: Context, from_cache: bool = True): + """ + Shows all users that are currently monitored and in which channel. + By default, the users are returned from the cache. + If this is not desired, `from_cache` can be given as a falsy value, e.g. e.g. 'no'. + """ + + if from_cache: + lines = tuple( + f"• <@{user_id}> in <#{self.watched_users[user_id].id}>" + for user_id in self.watched_users + ) + await LinePaginator.paginate( + lines or ("There's nothing here yet.",), + ctx, + Embed(title="Watched users (cached)", color=Color.blue()), + empty=False + ) + + else: + async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: + if response.status == 200: + data = await response.json() + self.update_cache(data) + lines = tuple(f"• <@{entry['user_id']}> in <#{entry['channel_id']}>" for entry in data) + + await LinePaginator.paginate( + lines or ("There's nothing here yet.",), + ctx, + Embed(title="Watched users", color=Color.blue()), + empty=False + ) + + else: + await ctx.send(f":x: got non-200 response from the API") + + @command(name='bigbrother.watch()', aliases=('bigbrother.watch',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watch_command(self, ctx: Context, user: User, channel: TextChannel = None): + """ + Relay messages sent by the given `user` in the given `channel`. + If `channel` is not specified, logs to the mod log channel. + """ + + if channel is not None: + channel_id = channel.id + else: + channel_id = Channels.message_change_logs + + post_data = { + 'user_id': str(user.id), + 'channel_id': str(channel_id) + } + + async with self.bot.http_session.post( + URLs.site_bigbrother_api, + headers=self.HEADERS, + json=post_data + ) as response: + if response.status == 204: + await ctx.send(f":ok_hand: will now relay messages sent by {user} in <#{channel_id}>") + + channel = self.bot.get_channel(channel_id) + if channel is None: + log.error( + f"could not update internal cache, failed to find a channel with ID {channel_id}" + ) + else: + self.watched_users[user.id] = channel + + else: + data = await response.json() + reason = data.get('error_message', "no message provided") + await ctx.send(f":x: the API returned an error: {reason}") + + @command(name='bigbrother.unwatch()', aliases=('bigbrother.unwatch',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def unwatch_command(self, ctx: Context, user: User): + """Stop relaying messages by the given `user`.""" + + url = f"{URLs.site_bigbrother_api}?user_id={user.id}" + async with self.bot.http_session.delete(url, headers=self.HEADERS) as response: + if response.status == 204: + await ctx.send(f":ok_hand: will no longer relay messages sent by {user}") + + if user.id in self.watched_users: + del self.watched_users[user.id] + else: + log.warning(f"user {user.id} was unwatched but was not found in the cache") + + else: + data = await response.json() + reason = data.get('error_message', "no message provided") + await ctx.send(f":x: the API returned an error: {reason}") + + +def setup(bot: Bot): + bot.add_cog(BigBrother(bot)) + log.info("Cog loaded: BigBrother") diff --git a/config-default.yml b/config-default.yml index 04ed53f7a..b0edb033d 100644 --- a/config-default.yml +++ b/config-default.yml @@ -10,6 +10,7 @@ bot: green_chevron: '<:greenchevron:418104310329769993>' red_chevron: '<:redchevron:418112778184818698>' white_chevron: '<:whitechevron:418110396973711363>' + lemoneye2: '<:lemoneye2:435193765582340098>' guild: @@ -28,6 +29,7 @@ guild: help4: 451312046647148554 helpers: 385474242440986624 modlog: 282638479504965634 + message_change_logs: 467752170159079424 python: 267624335836053506 verification: 352442727016693763 off_topic_0: 291284109232308226 @@ -74,6 +76,7 @@ urls: gitlab_bot_repo: 'https://gitlab.com/discord-python/projects/bot' omdb: 'http://omdbapi.com' site: 'pythondiscord.com' + site_bigbrother_api: 'https://api.pythondiscord.com/bot/bigbrother' site_docs_api: 'https://api.pythondiscord.com/bot/docs' site_facts_api: 'https://api.pythondiscord.com/bot/snake_facts' site_hiphopify_api: 'https://api.pythondiscord.com/bot/hiphopify' -- cgit v1.2.3