aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Chris Goes <[email protected]>2019-01-15 22:36:12 -0700
committerGravatar GitHub <[email protected]>2019-01-15 22:36:12 -0700
commitc21725e7f2ed6bde18b371143fc5b9bae0d7ff44 (patch)
tree9b33022fb6c2f47d13e1b71e4e431093e483bee2
parentFix number duplication (diff)
parentMerge pull request #256 from python-discord/fix-bb-emoji (diff)
Merge branch 'master' into update-contrib-doc
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/alias.py2
-rw-r--r--bot/cogs/antispam.py35
-rw-r--r--bot/cogs/bigbrother.py62
-rw-r--r--bot/cogs/events.py32
-rw-r--r--bot/cogs/free.py127
-rw-r--r--bot/cogs/help.py4
-rw-r--r--bot/cogs/information.py35
-rw-r--r--bot/cogs/modlog.py50
-rw-r--r--bot/cogs/tags.py2
-rw-r--r--bot/cogs/token_remover.py5
-rw-r--r--bot/constants.py15
-rw-r--r--bot/decorators.py36
-rw-r--r--bot/utils/checks.py56
-rw-r--r--config-default.yml16
15 files changed, 398 insertions, 80 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index 3c40a3243..581fa5c8e 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -75,6 +75,7 @@ bot.load_extension("bot.cogs.tags")
bot.load_extension("bot.cogs.token_remover")
bot.load_extension("bot.cogs.utils")
bot.load_extension("bot.cogs.wolfram")
+bot.load_extension("bot.cogs.free")
if has_rmq:
bot.load_extension("bot.cogs.rmq")
diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py
index 2ce4a51e3..0b848c773 100644
--- a/bot/cogs/alias.py
+++ b/bot/cogs/alias.py
@@ -71,7 +71,7 @@ class Alias:
@command(name="watch", hidden=True)
async def bigbrother_watch_alias(
- self, ctx, user: User, *, reason: str = None
+ self, ctx, user: User, *, reason: str
):
"""
Alias for invoking <prefix>bigbrother watch user [text_channel].
diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py
index d5b72718c..800700a50 100644
--- a/bot/cogs/antispam.py
+++ b/bot/cogs/antispam.py
@@ -1,21 +1,18 @@
-import asyncio
import logging
-import textwrap
from datetime import datetime, timedelta
from typing import List
-from dateutil.relativedelta import relativedelta
from discord import Colour, Member, Message, Object, TextChannel
from discord.ext.commands import Bot
from bot import rules
+from bot.cogs.moderation import Moderation
from bot.cogs.modlog import ModLog
from bot.constants import (
AntiSpam as AntiSpamConfig, Channels,
Colours, DEBUG_MODE, Event,
Guild as GuildConfig, Icons, Roles,
)
-from bot.utils.time import humanize_delta
log = logging.getLogger(__name__)
@@ -44,7 +41,7 @@ WHITELISTED_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)
class AntiSpam:
def __init__(self, bot: Bot):
self.bot = bot
- self.muted_role = None
+ self._muted_role = Object(Roles.muted)
@property
def mod_log(self) -> ModLog:
@@ -110,8 +107,6 @@ class AntiSpam:
# Sanity check to ensure we're not lagging behind
if self.muted_role not in member.roles:
remove_role_after = AntiSpamConfig.punishment['remove_after']
- duration_delta = relativedelta(seconds=remove_role_after)
- human_duration = humanize_delta(duration_delta)
mod_alert_message = (
f"**Triggered by:** {member.display_name}#{member.discriminator} (`{member.id}`)\n"
@@ -133,7 +128,8 @@ class AntiSpam:
mod_alert_message += f"{content}"
- await self.mod_log.send_log_message(
+ # Return the mod log message Context that we can use to post the infraction
+ mod_log_ctx = await self.mod_log.send_log_message(
icon_url=Icons.filtering,
colour=Colour(Colours.soft_red),
title=f"Spam detected!",
@@ -143,27 +139,8 @@ class AntiSpam:
ping_everyone=AntiSpamConfig.ping_everyone
)
- await member.add_roles(self.muted_role, reason=reason)
- description = textwrap.dedent(f"""
- **Channel**: {msg.channel.mention}
- **User**: {msg.author.mention} (`{msg.author.id}`)
- **Reason**: {reason}
- Role will be removed after {human_duration}.
- """)
-
- await self.mod_log.send_log_message(
- icon_url=Icons.user_mute, colour=Colour(Colours.soft_red),
- title="User muted", text=description
- )
-
- await asyncio.sleep(remove_role_after)
- await member.remove_roles(self.muted_role, reason="AntiSpam mute expired")
-
- await self.mod_log.send_log_message(
- icon_url=Icons.user_mute, colour=Colour(Colours.soft_green),
- title="User unmuted",
- text=f"Was muted by `AntiSpam` cog for {human_duration}."
- )
+ # Run a tempmute
+ await mod_log_ctx.invoke(Moderation.tempmute, member, f"{remove_role_after}S", reason=reason)
async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]):
# Is deletion of offending messages actually enabled?
diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py
index 29b13f038..70916cd7b 100644
--- a/bot/cogs/bigbrother.py
+++ b/bot/cogs/bigbrother.py
@@ -2,8 +2,10 @@ import asyncio
import logging
import re
from collections import defaultdict, deque
+from time import strptime, struct_time
from typing import List, Union
+from aiohttp import ClientError
from discord import Color, Embed, Guild, Member, Message, TextChannel, User
from discord.ext.commands import Bot, Context, group
@@ -26,9 +28,11 @@ class BigBrother:
def __init__(self, bot: Bot):
self.bot = bot
self.watched_users = {} # { user_id: log_channel_id }
+ self.watch_reasons = {} # { user_id: watch_reason }
self.channel_queues = defaultdict(lambda: defaultdict(deque)) # { user_id: { channel_id: queue(messages) }
self.last_log = [None, None, 0] # [user_id, channel_id, message_count]
self.consuming = False
+ self.infraction_watch_prefix = "bb watch: " # Please do not change or we won't be able to find old reasons
self.bot.loop.create_task(self.get_watched_users())
@@ -62,6 +66,42 @@ class BigBrother:
data = await response.json()
self.update_cache(data)
+ async def get_watch_reason(self, user_id: int) -> str:
+ """ Fetches and returns the latest watch reason for a user using the infraction API """
+
+ re_bb_watch = rf"^{self.infraction_watch_prefix}"
+ user_id = str(user_id)
+
+ try:
+ response = await self.bot.http_session.get(
+ URLs.site_infractions_user_type.format(
+ user_id=user_id,
+ infraction_type="note",
+ ),
+ params={"search": re_bb_watch, "hidden": "True", "active": "False"},
+ headers=self.HEADERS
+ )
+ infraction_list = await response.json()
+ except ClientError:
+ log.exception(f"Failed to retrieve bb watch reason for {user_id}.")
+ return "(error retrieving bb reason)"
+
+ if infraction_list:
+ latest_reason_infraction = max(infraction_list, key=self._parse_infraction_time)
+ latest_reason = latest_reason_infraction['reason'][len(self.infraction_watch_prefix):]
+ log.trace(f"The latest bb watch reason for {user_id}: {latest_reason}")
+ return latest_reason
+
+ log.trace(f"No bb watch reason found for {user_id}; returning default string")
+ return "(no reason specified)"
+
+ @staticmethod
+ def _parse_infraction_time(infraction: str) -> struct_time:
+ """Takes RFC1123 date_time string and returns time object for sorting purposes"""
+
+ date_string = infraction["inserted_at"]
+ return strptime(date_string, "%a, %d %b %Y %H:%M:%S %Z")
+
async def on_member_ban(self, guild: Guild, user: Union[User, Member]):
if guild.id == GuildConfig.id and user.id in self.watched_users:
url = f"{URLs.site_bigbrother_api}?user_id={user.id}"
@@ -70,6 +110,7 @@ class BigBrother:
async with self.bot.http_session.delete(url, headers=self.HEADERS) as response:
del self.watched_users[user.id]
del self.channel_queues[user.id]
+ del self.watch_reasons[user.id]
if response.status == 204:
await channel.send(
f"{Emojis.bb_message}:hammer: {user} got banned, so "
@@ -139,10 +180,17 @@ class BigBrother:
# Send header if user/channel are different or if message limit exceeded.
if message.author.id != last_user or message.channel.id != last_channel or msg_count > limit:
+ # Retrieve watch reason from API if it's not already in the cache
+ if message.author.id not in self.watch_reasons:
+ log.trace(f"No watch reason for {message.author.id} found in cache; retrieving from API")
+ user_watch_reason = await self.get_watch_reason(message.author.id)
+ self.watch_reasons[message.author.id] = user_watch_reason
+
self.last_log = [message.author.id, message.channel.id, 0]
embed = Embed(description=f"{message.author.mention} in [#{message.channel.name}]({message.jump_url})")
embed.set_author(name=message.author.nick or message.author.name, icon_url=message.author.avatar_url)
+ embed.set_footer(text=f"Watch reason: {self.watch_reasons[message.author.id]}")
await destination.send(embed=embed)
@staticmethod
@@ -246,15 +294,15 @@ class BigBrother:
)
else:
self.watched_users[user.id] = channel
+ self.watch_reasons[user.id] = reason
+ # Add a note (shadow warning) with the reason for watching
+ reason = f"{self.infraction_watch_prefix}{reason}"
+ await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)
else:
data = await response.json()
- reason = data.get('error_message', "no message provided")
- await ctx.send(f":x: the API returned an error: {reason}")
-
- # Add a note (shadow warning) with the reason for watching
- reason = "bb watch: " + reason # Prepend for situational awareness
- await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)
+ error_reason = data.get('error_message', "no message provided")
+ await ctx.send(f":x: the API returned an error: {error_reason}")
@bigbrother_group.command(name='unwatch', aliases=('uw',))
@with_role(Roles.owner, Roles.admin, Roles.moderator)
@@ -270,6 +318,8 @@ class BigBrother:
del self.watched_users[user.id]
if user.id in self.channel_queues:
del self.channel_queues[user.id]
+ if user.id in self.watch_reasons:
+ del self.watch_reasons[user.id]
else:
log.warning(f"user {user.id} was unwatched but was not found in the cache")
diff --git a/bot/cogs/events.py b/bot/cogs/events.py
index edfc6e579..f0baecd4b 100644
--- a/bot/cogs/events.py
+++ b/bot/cogs/events.py
@@ -25,6 +25,7 @@ class Events:
def __init__(self, bot: Bot):
self.bot = bot
+ self.headers = {"X-API-KEY": Keys.site_api}
@property
def mod_log(self) -> ModLog:
@@ -103,6 +104,29 @@ class Events:
resp = await response.json()
return resp["data"]
+ async def has_active_mute(self, user_id: str) -> bool:
+ """
+ Check whether a user has any active mute infractions
+ """
+
+ response = await self.bot.http_session.get(
+ URLs.site_infractions_user.format(
+ user_id=user_id
+ ),
+ params={"hidden": "True"},
+ headers=self.headers
+ )
+ infraction_list = await response.json()
+
+ # Check for active mute infractions
+ if not infraction_list:
+ # Short circuit
+ return False
+
+ return any(
+ infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute"
+ )
+
async def on_command_error(self, ctx: Context, e: CommandError):
command = ctx.command
parent = None
@@ -236,6 +260,14 @@ class Events:
for role in RESTORE_ROLES:
if role in old_roles:
+ # Check for mute roles that were not able to be removed and skip if present
+ if role == str(Roles.muted) and not await self.has_active_mute(str(member.id)):
+ log.debug(
+ f"User {member.id} has no active mute infraction, "
+ "their leftover muted role will not be persisted"
+ )
+ continue
+
new_roles.append(Object(int(role)))
for role in new_roles:
diff --git a/bot/cogs/free.py b/bot/cogs/free.py
new file mode 100644
index 000000000..620449f7e
--- /dev/null
+++ b/bot/cogs/free.py
@@ -0,0 +1,127 @@
+import logging
+from datetime import datetime
+
+from discord import Colour, Embed, Member, utils
+from discord.ext import commands
+from discord.ext.commands import BucketType, Context, command, cooldown
+
+from bot.constants import Categories, Free, Roles
+
+
+log = logging.getLogger(__name__)
+
+TIMEOUT = Free.activity_timeout
+RATE = Free.cooldown_rate
+PER = Free.cooldown_per
+
+
+class Free:
+ """Tries to figure out which help channels are free."""
+
+ PYTHON_HELP_ID = Categories.python_help
+
+ @command(name="free", aliases=('f',))
+ @cooldown(RATE, PER, BucketType.channel)
+ async def free(self, ctx: Context, user: Member = None, seek: int = 2):
+ """
+ Lists free help channels by likeliness of availability.
+ :param user: accepts user mention, ID, etc.
+ :param seek: How far back to check the last active message.
+
+ seek is used only when this command is invoked in a help channel.
+ You cannot override seek without mentioning a user first.
+
+ When seek is 2, we are avoiding considering the last active message
+ in a channel to be the one that invoked this command.
+
+ When seek is 3 or more, a user has been mentioned on the assumption
+ that they asked if the channel is free or they asked their question
+ in an active channel, and we want the message before that happened.
+ """
+ free_channels = []
+ python_help = utils.get(ctx.guild.categories, id=self.PYTHON_HELP_ID)
+
+ if user is not None and seek == 2:
+ seek = 3
+ elif not 0 < seek < 10:
+ seek = 3
+
+ # Iterate through all the help channels
+ # to check latest activity
+ for channel in python_help.channels:
+ # Seek further back in the help channel
+ # the command was invoked in
+ if channel.id == ctx.channel.id:
+ messages = await channel.history(limit=seek).flatten()
+ msg = messages[seek-1]
+ # Otherwise get last message
+ else:
+ msg = await channel.history(limit=1).next() # noqa (False positive)
+
+ inactive = (datetime.utcnow() - msg.created_at).seconds
+ if inactive > TIMEOUT:
+ free_channels.append((inactive, channel))
+
+ embed = Embed()
+ embed.colour = Colour.blurple()
+ embed.title = "**Looking for a free help channel?**"
+
+ if user is not None:
+ embed.description = f"**Hey {user.mention}!**\n\n"
+ else:
+ embed.description = ""
+
+ # Display all potentially inactive channels
+ # in descending order of inactivity
+ if free_channels:
+ embed.description += "**The following channel{0} look{1} free:**\n\n**".format(
+ 's' if len(free_channels) > 1 else '',
+ '' if len(free_channels) > 1 else 's'
+ )
+
+ # Sort channels in descending order by seconds
+ # Get position in list, inactivity, and channel object
+ # For each channel, add to embed.description
+ for i, (inactive, channel) in enumerate(sorted(free_channels, reverse=True), 1):
+ minutes, seconds = divmod(inactive, 60)
+ if minutes > 59:
+ hours, minutes = divmod(minutes, 60)
+ embed.description += f"{i}. {channel.mention} inactive for {hours}h{minutes}m{seconds}s\n\n"
+ else:
+ embed.description += f"{i}. {channel.mention} inactive for {minutes}m{seconds}s\n\n"
+
+ embed.description += ("**\nThese channels aren't guaranteed to be free, "
+ "so use your best judgement and check for yourself.")
+ else:
+ embed.description = ("**Doesn't look like any channels are available right now. "
+ "You're welcome to check for yourself to be sure. "
+ "If all channels are truly busy, please be patient "
+ "as one will likely be available soon.**")
+
+ await ctx.send(embed=embed)
+
+ @free.error
+ async def free_error(self, ctx: Context, error):
+ """
+ If error raised is CommandOnCooldown, and the
+ user who invoked has the helper role, reset
+ the cooldown and reinvoke the command.
+
+ Otherwise log the error.
+ """
+ helpers = ctx.guild.get_role(Roles.helpers)
+
+ if isinstance(error, commands.CommandOnCooldown):
+ if helpers in ctx.author.roles:
+ # reset cooldown so second invocation
+ # doesn't bring us back here.
+ ctx.command.reset_cooldown(ctx)
+ # return to avoid needlessly logging the error
+ return await ctx.reinvoke()
+
+ log.exception(error) # Don't ignore other errors
+
+
+def setup(bot):
+ bot.add_cog(Free())
+ log.info("Cog loaded: Free")
diff --git a/bot/cogs/help.py b/bot/cogs/help.py
index c82a25417..ded068123 100644
--- a/bot/cogs/help.py
+++ b/bot/cogs/help.py
@@ -6,10 +6,10 @@ from contextlib import suppress
from discord import Colour, Embed, HTTPException
from discord.ext import commands
+from discord.ext.commands import CheckFailure
from fuzzywuzzy import fuzz, process
from bot import constants
-from bot.decorators import InChannelCheckFailure
from bot.pagination import (
DELETE_EMOJI, FIRST_EMOJI, LAST_EMOJI,
LEFT_EMOJI, LinePaginator, RIGHT_EMOJI,
@@ -435,7 +435,7 @@ class HelpSession:
# the mean time.
try:
can_run = await command.can_run(self._ctx)
- except InChannelCheckFailure:
+ except CheckFailure:
can_run = False
if not can_run:
diff --git a/bot/cogs/information.py b/bot/cogs/information.py
index 7a244cdbe..129166d2f 100644
--- a/bot/cogs/information.py
+++ b/bot/cogs/information.py
@@ -1,11 +1,13 @@
import logging
+import random
import textwrap
from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel
-from discord.ext.commands import Bot, Context, command
+from discord.ext.commands import BadArgument, Bot, CommandError, Context, MissingPermissions, command
-from bot.constants import Emojis, Keys, Roles, URLs
+from bot.constants import Channels, Emojis, Keys, NEGATIVE_REPLIES, Roles, URLs
from bot.decorators import with_role
+from bot.utils.checks import with_role_check
from bot.utils.time import time_since
log = logging.getLogger(__name__)
@@ -121,13 +123,23 @@ class Information:
await ctx.send(embed=embed)
- @with_role(*MODERATION_ROLES)
@command(name="user", aliases=["user_info", "member", "member_info"])
async def user_info(self, ctx: Context, user: Member = None, hidden: bool = False):
"""
Returns info about a user.
"""
+ # Do a role check if this is being executed on
+ # someone other than the caller
+ if user and user != ctx.author:
+ if not with_role_check(ctx, *MODERATION_ROLES):
+ raise BadArgument("You do not have permission to use this command on users other than yourself.")
+
+ # Non-moderators may only do this in #bot-commands
+ if not with_role_check(ctx, *MODERATION_ROLES):
+ if not ctx.channel.id == Channels.bot:
+ raise MissingPermissions("You can't do that here!")
+
# Validates hidden input
hidden = str(hidden)
@@ -192,6 +204,23 @@ class Information:
await ctx.send(embed=embed)
+ @user_info.error
+ async def user_info_command_error(self, ctx: Context, error: CommandError):
+ embed = Embed(colour=Colour.red())
+
+ if isinstance(error, BadArgument):
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = str(error)
+ await ctx.send(embed=embed)
+
+ elif isinstance(error, MissingPermissions):
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = f"Sorry, but you may only use this command within <#{Channels.bot}>."
+ await ctx.send(embed=embed)
+
+ else:
+ log.exception(f"Unhandled error: {error}")
+
def setup(bot):
bot.add_cog(Information(bot))
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
index c96838a54..55611c5e4 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -104,9 +104,19 @@ class ModLog:
self._ignored[event].append(item)
async def send_log_message(
- self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str,
- thumbnail: str = None, channel_id: int = Channels.modlog, ping_everyone: bool = False,
- files: List[File] = None, content: str = None, additional_embeds: List[Embed] = None,
+ self,
+ icon_url: Optional[str],
+ colour: Colour,
+ title: Optional[str],
+ text: str,
+ thumbnail: Optional[str] = None,
+ channel_id: int = Channels.modlog,
+ ping_everyone: bool = False,
+ files: Optional[List[File]] = None,
+ content: Optional[str] = None,
+ additional_embeds: Optional[List[Embed]] = None,
+ timestamp_override: Optional[datetime.datetime] = None,
+ footer_override: Optional[str] = None,
):
embed = Embed(description=text)
@@ -114,9 +124,13 @@ class ModLog:
embed.set_author(name=title, icon_url=icon_url)
embed.colour = colour
- embed.timestamp = datetime.datetime.utcnow()
- if thumbnail is not None:
+ embed.timestamp = timestamp_override or datetime.datetime.utcnow()
+
+ if footer_override:
+ embed.set_footer(text=footer_override)
+
+ if thumbnail:
embed.set_thumbnail(url=thumbnail)
if ping_everyone:
@@ -126,14 +140,15 @@ class ModLog:
content = "@everyone"
channel = self.bot.get_channel(channel_id)
-
- await channel.send(content=content, embed=embed, files=files)
+ log_message = await channel.send(content=content, embed=embed, files=files)
if additional_embeds:
await channel.send("With the following embed(s):")
for additional_embed in additional_embeds:
await channel.send(embed=additional_embed)
+ return await self.bot.get_context(log_message) # Optionally return for use with antispam
+
async def on_guild_channel_create(self, channel: GUILD_CHANNEL):
if channel.guild.id != GuildConstant.id:
return
@@ -675,14 +690,27 @@ class ModLog:
f"{after.clean_content}"
)
+ if before.edited_at:
+ # Message was previously edited, to assist with self-bot detection, use the edited_at
+ # datetime as the baseline and create a human-readable delta between this edit event
+ # and the last time the message was edited
+ timestamp = before.edited_at
+ delta = humanize_delta(relativedelta(after.edited_at, before.edited_at))
+ footer = f"Last edited {delta} ago"
+ else:
+ # Message was not previously edited, use the created_at datetime as the baseline, no
+ # delta calculation needed
+ timestamp = before.created_at
+ footer = None
+
await self.send_log_message(
- Icons.message_edit, Colour.blurple(), "Message edited (Before)",
- before_response, channel_id=Channels.message_log
+ Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response,
+ channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer
)
await self.send_log_message(
- Icons.message_edit, Colour.blurple(), "Message edited (After)",
- after_response, channel_id=Channels.message_log
+ Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response,
+ channel_id=Channels.message_log, timestamp_override=after.edited_at
)
async def on_raw_message_edit(self, event: RawMessageUpdateEvent):
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index b128b6de1..8ecd80127 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -149,7 +149,7 @@ class Tags:
tags = []
- embed = Embed()
+ embed: Embed = Embed()
embed.colour = Colour.red()
tag_data = await self.get_tag_data(tag_name)
diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py
index 8277513a7..c1a0e18ba 100644
--- a/bot/cogs/token_remover.py
+++ b/bot/cogs/token_remover.py
@@ -16,8 +16,9 @@ log = logging.getLogger(__name__)
DELETION_MESSAGE_TEMPLATE = (
"Hey {mention}! I noticed you posted a seemingly valid Discord API "
- "token in your message and have removed your message to prevent abuse. "
- "We recommend regenerating your token regardless, which you can do here: "
+ "token in your message and have removed your message. "
+ "We **strongly recommend** regenerating your token as it's probably "
+ "been compromised. You can do that here: "
"<https://discordapp.com/developers/applications/me>\n"
"Feel free to re-post it with the token removed. "
"If you believe this was a mistake, please let us know!"
diff --git a/bot/constants.py b/bot/constants.py
index c1375bb13..be713cef2 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -317,6 +317,13 @@ class CleanMessages(metaclass=YAMLGetter):
message_limit: int
+class Categories(metaclass=YAMLGetter):
+ section = "guild"
+ subsection = "categories"
+
+ python_help: int
+
+
class Channels(metaclass=YAMLGetter):
section = "guild"
subsection = "channels"
@@ -466,6 +473,14 @@ class BigBrother(metaclass=YAMLGetter):
header_message_limit: int
+class Free(metaclass=YAMLGetter):
+ section = 'free'
+
+ activity_timeout: int
+ cooldown_rate: int
+ cooldown_per: float
+
+
# Debug mode
DEBUG_MODE = True if 'local' in os.environ.get("SITE_URL", "local") else False
diff --git a/bot/decorators.py b/bot/decorators.py
index 87877ecbf..710045c10 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -10,6 +10,7 @@ from discord.ext import commands
from discord.ext.commands import CheckFailure, Context
from bot.constants import ERROR_REPLIES
+from bot.utils.checks import with_role_check, without_role_check
log = logging.getLogger(__name__)
@@ -47,35 +48,24 @@ def in_channel(*channels: int, bypass_roles: typing.Container[int] = None):
def with_role(*role_ids: int):
- async def predicate(ctx: Context):
- if not ctx.guild: # Return False in a DM
- log.debug(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
- "This command is restricted by the with_role decorator. Rejecting request.")
- return False
-
- for role in ctx.author.roles:
- if role.id in role_ids:
- log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.")
- return True
+ """
+ Returns True if the user has any one
+ of the roles in role_ids.
+ """
- log.debug(f"{ctx.author} does not have the required role to use "
- f"the '{ctx.command.name}' command, so the request is rejected.")
- return False
+ async def predicate(ctx: Context):
+ return with_role_check(ctx, *role_ids)
return commands.check(predicate)
def without_role(*role_ids: int):
- async def predicate(ctx: Context):
- if not ctx.guild: # Return False in a DM
- log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
- "This command is restricted by the without_role decorator. Rejecting request.")
- return False
+ """
+ Returns True if the user does not have any
+ of the roles in role_ids.
+ """
- author_roles = [role.id for role in ctx.author.roles]
- check = all(role not in author_roles for role in role_ids)
- log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the without_role check was {check}.")
- return check
+ async def predicate(ctx: Context):
+ return without_role_check(ctx, *role_ids)
return commands.check(predicate)
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
new file mode 100644
index 000000000..37dc657f7
--- /dev/null
+++ b/bot/utils/checks.py
@@ -0,0 +1,56 @@
+import logging
+
+from discord.ext.commands import Context
+
+log = logging.getLogger(__name__)
+
+
+def with_role_check(ctx: Context, *role_ids: int) -> bool:
+ """
+ Returns True if the user has any one
+ of the roles in role_ids.
+ """
+
+ if not ctx.guild: # Return False in a DM
+ log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
+ "This command is restricted by the with_role decorator. Rejecting request.")
+ return False
+
+ for role in ctx.author.roles:
+ if role.id in role_ids:
+ log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.")
+ return True
+
+ log.trace(f"{ctx.author} does not have the required role to use "
+ f"the '{ctx.command.name}' command, so the request is rejected.")
+ return False
+
+
+def without_role_check(ctx: Context, *role_ids: int) -> bool:
+ """
+ Returns True if the user does not have any
+ of the roles in role_ids.
+ """
+
+ if not ctx.guild: # Return False in a DM
+ log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
+ "This command is restricted by the without_role decorator. Rejecting request.")
+ return False
+
+ author_roles = (role.id for role in ctx.author.roles)
+ check = all(role not in author_roles for role in role_ids)
+ log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the without_role check was {check}.")
+ return check
+
+
+def in_channel_check(ctx: Context, channel_id: int) -> bool:
+ """
+ Checks if the command was executed
+ inside of the specified channel.
+ """
+
+ check = ctx.channel.id == channel_id
+ log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the in_channel check was {check}.")
+ return check
diff --git a/config-default.yml b/config-default.yml
index 21d7f20b9..b6427b489 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -25,7 +25,7 @@ style:
green_chevron: "<:greenchevron:418104310329769993>"
red_chevron: "<:redchevron:418112778184818698>"
white_chevron: "<:whitechevron:418110396973711363>"
- bb_message: "<:bbmessage:472476937504423936>"
+ bb_message: "<:bbmessage:476273120999636992>"
status_online: "<:status_online:470326272351010816>"
status_idle: "<:status_idle:470326266625785866>"
@@ -85,6 +85,9 @@ style:
guild:
id: 267624335836053506
+ categories:
+ python_help: 356013061213126657
+
channels:
admins: &ADMINS 365960823622991872
announcements: 354619224620138496
@@ -137,7 +140,7 @@ filter:
filter_zalgo: false
filter_invites: true
filter_domains: true
- filter_rich_embeds: true
+ filter_rich_embeds: false
watch_words: true
watch_tokens: true
@@ -240,6 +243,7 @@ urls:
site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"]
site_infractions_by_id: !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"]
site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"]
+ site_infractions_user_type: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"]
site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"]
site_logs_view: !JOIN [*SCHEMA, *DOMAIN, "/bot/logs"]
site_names_api: !JOIN [*SCHEMA, *API, "/bot/snake_names"]
@@ -336,5 +340,13 @@ big_brother:
header_message_limit: 15
+free:
+ # Seconds to elapse for a channel
+ # to be considered inactive.
+ activity_timeout: 600
+ cooldown_rate: 1
+ cooldown_per: 60.0
+
+
config:
required_keys: ['bot.token']