aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/__main__.py5
-rw-r--r--bot/constants.py9
-rw-r--r--bot/exts/christmas/advent_of_code/_cog.py20
-rw-r--r--bot/exts/evergreen/cheatsheet.py107
-rw-r--r--bot/exts/evergreen/conversationstarters.py4
-rw-r--r--bot/exts/evergreen/status_cats.py33
-rw-r--r--bot/exts/evergreen/status_codes.py71
-rw-r--r--bot/exts/evergreen/wolfram.py11
-rw-r--r--bot/exts/evergreen/xkcd.py2
-rw-r--r--bot/exts/halloween/hacktoberstats.py8
-rw-r--r--bot/exts/valentines/be_my_valentine.py80
-rw-r--r--bot/exts/valentines/lovecalculator.py11
-rw-r--r--bot/utils/decorators.py119
13 files changed, 306 insertions, 174 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index e9b14a53..c6e5fa57 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -6,10 +6,9 @@ from sentry_sdk.integrations.redis import RedisIntegration
from bot.bot import bot
from bot.constants import Client, GIT_SHA, STAFF_ROLES, WHITELISTED_CHANNELS
-from bot.utils.decorators import in_channel_check
+from bot.utils.decorators import whitelist_check
from bot.utils.extensions import walk_extensions
-
sentry_logging = LoggingIntegration(
level=logging.DEBUG,
event_level=logging.WARNING
@@ -26,7 +25,7 @@ sentry_sdk.init(
log = logging.getLogger(__name__)
-bot.add_check(in_channel_check(*WHITELISTED_CHANNELS, bypass_roles=STAFF_ROLES))
+bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES))
for ext in walk_extensions():
bot.load_extension(ext)
diff --git a/bot/constants.py b/bot/constants.py
index 1234ef3b..bb538487 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -106,12 +106,6 @@ class Channels(NamedTuple):
devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))
dev_contrib = 635950537262759947
dev_branding = 753252897059373066
- help_0 = 303906576991780866
- help_1 = 303906556754395136
- help_2 = 303906514266226689
- help_3 = 439702951246692352
- help_4 = 451312046647148554
- help_5 = 454941769734422538
helpers = 385474242440986624
message_log = 467752170159079424
mod_alerts = 473092532147060736
@@ -133,6 +127,7 @@ class Channels(NamedTuple):
class Categories(NamedTuple):
+ help_in_use = 696958401460043776
development = 411199786025484308
devprojects = 787641585624940544
media = 799054581991997460
@@ -248,7 +243,6 @@ class Roles(NamedTuple):
announcements = 463658397560995840
champion = 430492892331769857
contributor = 295488872404484098
- developer = 352427296948486144
devops = 409416496733880320
jammer = 423054537079783434
moderator = 267629731250176001
@@ -259,6 +253,7 @@ class Roles(NamedTuple):
rockstars = 458226413825294336
core_developers = 587606783669829632
events_lead = 778361735739998228
+ everyone_role = 267624335836053506
class Tokens(NamedTuple):
diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py
index c3b87f96..466edd48 100644
--- a/bot/exts/christmas/advent_of_code/_cog.py
+++ b/bot/exts/christmas/advent_of_code/_cog.py
@@ -11,7 +11,7 @@ from bot.constants import (
AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS,
)
from bot.exts.christmas.advent_of_code import _helpers
-from bot.utils.decorators import InChannelCheckFailure, in_month, override_in_channel, with_role
+from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role
log = logging.getLogger(__name__)
@@ -50,7 +50,7 @@ class AdventOfCode(commands.Cog):
self.status_task.add_done_callback(_helpers.background_task_callback)
@commands.group(name="adventofcode", aliases=("aoc",))
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def adventofcode_group(self, ctx: commands.Context) -> None:
"""All of the Advent of Code commands."""
if not ctx.invoked_subcommand:
@@ -61,7 +61,7 @@ class AdventOfCode(commands.Cog):
aliases=("sub", "notifications", "notify", "notifs"),
brief="Notifications for new days"
)
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def aoc_subscribe(self, ctx: commands.Context) -> None:
"""Assign the role for notifications about new days being ready."""
current_year = datetime.now().year
@@ -82,7 +82,7 @@ class AdventOfCode(commands.Cog):
@in_month(Month.DECEMBER)
@adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def aoc_unsubscribe(self, ctx: commands.Context) -> None:
"""Remove the role for notifications about new days being ready."""
role = ctx.guild.get_role(AocConfig.role_id)
@@ -94,7 +94,7 @@ class AdventOfCode(commands.Cog):
await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")
@adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def aoc_countdown(self, ctx: commands.Context) -> None:
"""Return time left until next day."""
if not _helpers.is_in_advent():
@@ -123,13 +123,13 @@ class AdventOfCode(commands.Cog):
await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.")
@adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code")
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def about_aoc(self, ctx: commands.Context) -> None:
"""Respond with an explanation of all things Advent of Code."""
await ctx.send("", embed=self.cached_about_aoc)
@adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)")
- @override_in_channel(AOC_WHITELIST)
+ @whitelist_override(channels=AOC_WHITELIST)
async def join_leaderboard(self, ctx: commands.Context) -> None:
"""DM the user the information for joining the Python Discord leaderboard."""
current_year = datetime.now().year
@@ -178,7 +178,7 @@ class AdventOfCode(commands.Cog):
aliases=("board", "lb"),
brief="Get a snapshot of the PyDis private AoC leaderboard",
)
- @override_in_channel(AOC_WHITELIST_RESTRICTED)
+ @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
async def aoc_leaderboard(self, ctx: commands.Context) -> None:
"""Get the current top scorers of the Python Discord Leaderboard."""
async with ctx.typing():
@@ -203,7 +203,7 @@ class AdventOfCode(commands.Cog):
aliases=("globalboard", "gb"),
brief="Get a link to the global leaderboard",
)
- @override_in_channel(AOC_WHITELIST_RESTRICTED)
+ @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
async def aoc_global_leaderboard(self, ctx: commands.Context) -> None:
"""Get a link to the global Advent of Code leaderboard."""
url = self.global_leaderboard_url
@@ -219,7 +219,7 @@ class AdventOfCode(commands.Cog):
aliases=("dailystats", "ds"),
brief="Get daily statistics for the Python Discord leaderboard"
)
- @override_in_channel(AOC_WHITELIST_RESTRICTED)
+ @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
"""Send an embed with daily completion statistics for the Python Discord leaderboard."""
try:
diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py
new file mode 100644
index 00000000..3fe709d5
--- /dev/null
+++ b/bot/exts/evergreen/cheatsheet.py
@@ -0,0 +1,107 @@
+import random
+import re
+import typing as t
+from urllib.parse import quote_plus
+
+from discord import Embed
+from discord.ext import commands
+from discord.ext.commands import BucketType, Context
+
+from bot import constants
+from bot.constants import Categories, Channels, Colours, ERROR_REPLIES
+from bot.utils.decorators import whitelist_override
+
+ERROR_MESSAGE = f"""
+Unknown cheat sheet. Please try to reformulate your query.
+
+**Examples**:
+```md
+{constants.Client.prefix}cht read json
+{constants.Client.prefix}cht hello world
+{constants.Client.prefix}cht lambda
+```
+If the problem persists send a message in <#{Channels.dev_contrib}>
+"""
+
+URL = 'https://cheat.sh/python/{search}'
+ESCAPE_TT = str.maketrans({"`": "\\`"})
+ANSI_RE = re.compile(r"\x1b\[.*?m")
+# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html.
+HEADERS = {'User-Agent': 'curl/7.68.0'}
+
+
+class CheatSheet(commands.Cog):
+ """Commands that sends a result of a cht.sh search in code blocks."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @staticmethod
+ def fmt_error_embed() -> Embed:
+ """
+ Format the Error Embed.
+
+ If the cht.sh search returned 404, overwrite it to send a custom error embed.
+ link -> https://github.com/chubin/cheat.sh/issues/198
+ """
+ embed = Embed(
+ title=random.choice(ERROR_REPLIES),
+ description=ERROR_MESSAGE,
+ colour=Colours.soft_red
+ )
+ return embed
+
+ def result_fmt(self, url: str, body_text: str) -> t.Tuple[bool, t.Union[str, Embed]]:
+ """Format Result."""
+ if body_text.startswith("# 404 NOT FOUND"):
+ embed = self.fmt_error_embed()
+ return True, embed
+
+ body_space = min(1986 - len(url), 1000)
+
+ if len(body_text) > body_space:
+ description = (f"**Result Of cht.sh**\n"
+ f"```python\n{body_text[:body_space]}\n"
+ f"... (truncated - too many lines)```\n"
+ f"Full results: {url} ")
+ else:
+ description = (f"**Result Of cht.sh**\n"
+ f"```python\n{body_text}```\n"
+ f"{url}")
+ return False, description
+
+ @commands.command(
+ name="cheat",
+ aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"),
+ )
+ @commands.cooldown(1, 10, BucketType.user)
+ @whitelist_override(categories=[Categories.help_in_use])
+ async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None:
+ """
+ Search cheat.sh.
+
+ Gets a post from https://cheat.sh/python/ by default.
+ Usage:
+ --> .cht read json
+ """
+ async with ctx.typing():
+ search_string = quote_plus(" ".join(search_terms))
+
+ async with self.bot.http_session.get(
+ URL.format(search=search_string), headers=HEADERS
+ ) as response:
+ result = ANSI_RE.sub("", await response.text()).translate(ESCAPE_TT)
+
+ is_embed, description = self.result_fmt(
+ URL.format(search=search_string),
+ result
+ )
+ if is_embed:
+ await ctx.send(embed=description)
+ else:
+ await ctx.send(content=description)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the CheatSheet cog."""
+ bot.add_cog(CheatSheet(bot))
diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py
index 576b8d76..e7058961 100644
--- a/bot/exts/evergreen/conversationstarters.py
+++ b/bot/exts/evergreen/conversationstarters.py
@@ -5,7 +5,7 @@ from discord import Color, Embed
from discord.ext import commands
from bot.constants import WHITELISTED_CHANNELS
-from bot.utils.decorators import override_in_channel
+from bot.utils.decorators import whitelist_override
from bot.utils.randomization import RandomCycle
SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9'
@@ -38,7 +38,7 @@ class ConvoStarters(commands.Cog):
self.bot = bot
@commands.command()
- @override_in_channel(ALL_ALLOWED_CHANNELS)
+ @whitelist_override(channels=ALL_ALLOWED_CHANNELS)
async def topic(self, ctx: commands.Context) -> None:
"""
Responds with a random topic to start a conversation.
diff --git a/bot/exts/evergreen/status_cats.py b/bot/exts/evergreen/status_cats.py
deleted file mode 100644
index 586b8378..00000000
--- a/bot/exts/evergreen/status_cats.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from http import HTTPStatus
-
-import discord
-from discord.ext import commands
-
-
-class StatusCats(commands.Cog):
- """Commands that give HTTP statuses described and visualized by cats."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=['statuscat'])
- async def http_cat(self, ctx: commands.Context, code: int) -> None:
- """Sends an embed with an image of a cat, potraying the status code."""
- embed = discord.Embed(title=f'**Status: {code}**')
-
- try:
- HTTPStatus(code)
-
- except ValueError:
- embed.set_footer(text='Inputted status code does not exist.')
-
- else:
- embed.set_image(url=f'https://http.cat/{code}.jpg')
-
- finally:
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the StatusCats cog."""
- bot.add_cog(StatusCats(bot))
diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py
new file mode 100644
index 00000000..874c87eb
--- /dev/null
+++ b/bot/exts/evergreen/status_codes.py
@@ -0,0 +1,71 @@
+from http import HTTPStatus
+
+import discord
+from discord.ext import commands
+
+HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg"
+HTTP_CAT_URL = "https://http.cat/{code}.jpg"
+
+
+class HTTPStatusCodes(commands.Cog):
+ """Commands that give HTTP statuses described and visualized by cats and dogs."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.group(name="http_status", aliases=("status", "httpstatus"))
+ async def http_status_group(self, ctx: commands.Context) -> None:
+ """Group containing dog and cat http status code commands."""
+ if not ctx.invoked_subcommand:
+ await ctx.send_help(ctx.command)
+
+ @http_status_group.command(name='cat')
+ async def http_cat(self, ctx: commands.Context, code: int) -> None:
+ """Sends an embed with an image of a cat, portraying the status code."""
+ embed = discord.Embed(title=f'**Status: {code}**')
+ url = HTTP_CAT_URL.format(code=code)
+
+ try:
+ HTTPStatus(code)
+ async with self.bot.http_session.get(url, allow_redirects=False) as response:
+ if response.status != 404:
+ embed.set_image(url=url)
+ else:
+ raise NotImplementedError
+
+ except ValueError:
+ embed.set_footer(text='Inputted status code does not exist.')
+
+ except NotImplementedError:
+ embed.set_footer(text='Inputted status code is not implemented by http.cat yet.')
+
+ finally:
+ await ctx.send(embed=embed)
+
+ @http_status_group.command(name='dog')
+ async def http_dog(self, ctx: commands.Context, code: int) -> None:
+ """Sends an embed with an image of a dog, portraying the status code."""
+ embed = discord.Embed(title=f'**Status: {code}**')
+ url = HTTP_DOG_URL.format(code=code)
+
+ try:
+ HTTPStatus(code)
+ async with self.bot.http_session.get(url, allow_redirects=False) as response:
+ if response.status != 302:
+ embed.set_image(url=url)
+ else:
+ raise NotImplementedError
+
+ except ValueError:
+ embed.set_footer(text='Inputted status code does not exist.')
+
+ except NotImplementedError:
+ embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.')
+
+ finally:
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the HTTPStatusCodes cog."""
+ bot.add_cog(HTTPStatusCodes(bot))
diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py
index 898e8d2a..437d9e1a 100644
--- a/bot/exts/evergreen/wolfram.py
+++ b/bot/exts/evergreen/wolfram.py
@@ -108,7 +108,10 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional
"input": query,
"appid": APPID,
"output": DEFAULT_OUTPUT_FORMAT,
- "format": "image,plaintext"
+ "format": "image,plaintext",
+ "location": "the moon",
+ "latlong": "0.0,0.0",
+ "ip": "1.1.1.1"
})
request_url = QUERY.format(request="query", data=url_str)
@@ -168,6 +171,9 @@ class Wolfram(Cog):
url_str = parse.urlencode({
"i": query,
"appid": APPID,
+ "location": "the moon",
+ "latlong": "0.0,0.0",
+ "ip": "1.1.1.1"
})
query = QUERY.format(request="simple", data=url_str)
@@ -248,6 +254,9 @@ class Wolfram(Cog):
url_str = parse.urlencode({
"i": query,
"appid": APPID,
+ "location": "the moon",
+ "latlong": "0.0,0.0",
+ "ip": "1.1.1.1"
})
query = QUERY.format(request="result", data=url_str)
diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py
index d3224bfe..1ff98ca2 100644
--- a/bot/exts/evergreen/xkcd.py
+++ b/bot/exts/evergreen/xkcd.py
@@ -69,6 +69,8 @@ class XKCD(Cog):
return
embed.title = f"XKCD comic #{info['num']}"
+ embed.description = info['alt']
+ embed.url = f"{BASE_URL}/{info['num']}"
if info["img"][-3:] in ("jpg", "png", "gif"):
embed.set_image(url=info["img"])
diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py
index a1c55922..d9fc0e8a 100644
--- a/bot/exts/halloween/hacktoberstats.py
+++ b/bot/exts/halloween/hacktoberstats.py
@@ -11,7 +11,7 @@ from async_rediscache import RedisCache
from discord.ext import commands
from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS
-from bot.utils.decorators import in_month, override_in_channel
+from bot.utils.decorators import in_month, whitelist_override
log = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ class HacktoberStats(commands.Cog):
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True)
- @override_in_channel(HACKTOBER_WHITELIST)
+ @whitelist_override(channels=HACKTOBER_WHITELIST)
async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None:
"""
Display an embed for a user's Hacktoberfest contributions.
@@ -72,7 +72,7 @@ class HacktoberStats(commands.Cog):
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@hacktoberstats_group.command(name="link")
- @override_in_channel(HACKTOBER_WHITELIST)
+ @whitelist_override(channels=HACKTOBER_WHITELIST)
async def link_user(self, ctx: commands.Context, github_username: str = None) -> None:
"""
Link the invoking user's Github github_username to their Discord ID.
@@ -96,7 +96,7 @@ class HacktoberStats(commands.Cog):
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@hacktoberstats_group.command(name="unlink")
- @override_in_channel(HACKTOBER_WHITELIST)
+ @whitelist_override(channels=HACKTOBER_WHITELIST)
async def unlink_user(self, ctx: commands.Context) -> None:
"""Remove the invoking user's account link from the log."""
author_id, author_mention = self._author_mention_from_context(ctx)
diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py
index 4db4d191..f3392bcb 100644
--- a/bot/exts/valentines/be_my_valentine.py
+++ b/bot/exts/valentines/be_my_valentine.py
@@ -2,13 +2,13 @@ import logging
import random
from json import load
from pathlib import Path
-from typing import Optional, Tuple
+from typing import Tuple
import discord
from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType
-from bot.constants import Channels, Client, Colours, Lovefest, Month
+from bot.constants import Channels, Colours, Lovefest, Month
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
@@ -70,44 +70,35 @@ class BeMyValentine(commands.Cog):
@commands.cooldown(1, 1800, BucketType.user)
@commands.group(name='bemyvalentine', invoke_without_command=True)
async def send_valentine(
- self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None
+ self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
"""
- Send a valentine to user, if specified, or to a random user with the lovefest role.
+ Send a valentine to a specified user with the lovefest role.
- syntax: .bemyvalentine [user](optional) [p/poem/c/compliment/or you can type your own valentine message]
+ syntax: .bemyvalentine [user] [p/poem/c/compliment/or you can type your own valentine message]
(optional)
- example: .bemyvalentine (sends valentine as a poem or a compliment to a random user)
example: .bemyvalentine Iceman#6508 p (sends a poem to Iceman)
example: .bemyvalentine Iceman Hey I love you, wanna hang around ? (sends the custom message to Iceman)
NOTE : AVOID TAGGING THE USER MOST OF THE TIMES.JUST TRIM THE '@' when using this command.
"""
if ctx.guild is None:
# This command should only be used in the server
- msg = "You are supposed to use this command in the server."
- return await ctx.send(msg)
+ raise commands.UserInputError("You are supposed to use this command in the server.")
- if user:
- if Lovefest.role_id not in [role.id for role in user.roles]:
- message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!"
- return await ctx.send(message)
+ if Lovefest.role_id not in [role.id for role in user.roles]:
+ raise commands.UserInputError(
+ f"You cannot send a valentine to {user} as they do not have the lovefest role!"
+ )
if user == ctx.author:
# Well a user can't valentine himself/herself.
- return await ctx.send("Come on dude, you can't send a valentine to yourself :expressionless:")
+ raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:")
emoji_1, emoji_2 = self.random_emoji()
- lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
channel = self.bot.get_channel(Channels.community_bot_commands)
valentine, title = self.valentine_check(valentine_type)
- if user is None:
- author = ctx.author
- user = self.random_user(author, lovefest_role.members)
- if user is None:
- return await ctx.send("There are no users avilable to whome your valentine can be sent.")
-
embed = discord.Embed(
title=f'{emoji_1} {title} {user.display_name} {emoji_2}',
description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**',
@@ -118,56 +109,41 @@ class BeMyValentine(commands.Cog):
@commands.cooldown(1, 1800, BucketType.user)
@send_valentine.command(name='secret')
async def anonymous(
- self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None
+ self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
"""
- Send an anonymous Valentine via DM to to a user, if specified, or to a random with the lovefest role.
-
- **This command should be DMed to the bot.**
+ Send an anonymous Valentine via DM to to a specified user with the lovefest role.
- syntax : .bemyvalentine secret [user](optional) [p/poem/c/compliment/or you can type your own valentine message]
+ syntax : .bemyvalentine secret [user] [p/poem/c/compliment/or you can type your own valentine message]
(optional)
- example : .bemyvalentine secret (sends valentine as a poem or a compliment to a random user in DM making you
- anonymous)
example : .bemyvalentine secret Iceman#6508 p (sends a poem to Iceman in DM making you anonymous)
example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to
Iceman in DM making you anonymous)
"""
- if ctx.guild is not None:
- # This command is only DM specific
- msg = "You are not supposed to use this command in the server, DM the command to the bot."
- return await ctx.send(msg)
-
- if user:
- if Lovefest.role_id not in [role.id for role in user.roles]:
- message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!"
- return await ctx.send(message)
+ if Lovefest.role_id not in [role.id for role in user.roles]:
+ await ctx.message.delete()
+ raise commands.UserInputError(
+ f"You cannot send a valentine to {user} as they do not have the lovefest role!"
+ )
if user == ctx.author:
# Well a user cant valentine himself/herself.
- return await ctx.send('Come on dude, you cant send a valentine to yourself :expressionless:')
+ raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:")
- guild = self.bot.get_guild(id=Client.guild)
emoji_1, emoji_2 = self.random_emoji()
- lovefest_role = discord.utils.get(guild.roles, id=Lovefest.role_id)
valentine, title = self.valentine_check(valentine_type)
- if user is None:
- author = ctx.author
- user = self.random_user(author, lovefest_role.members)
- if user is None:
- return await ctx.send("There are no users avilable to whome your valentine can be sent.")
-
embed = discord.Embed(
title=f'{emoji_1}{title} {user.display_name}{emoji_2}',
description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**',
color=Colours.pink
)
+ await ctx.message.delete()
try:
await user.send(embed=embed)
except discord.Forbidden:
- await ctx.author.send(f"{user} has DMs disabled, so I couldn't send the message. Sorry!")
+ raise commands.UserInputError(f"{user} has DMs disabled, so I couldn't send the message. Sorry!")
else:
await ctx.author.send(f"Your message has been sent to {user}")
@@ -191,18 +167,6 @@ class BeMyValentine(commands.Cog):
return valentine, title
@staticmethod
- def random_user(author: discord.Member, members: discord.Member) -> None:
- """
- Picks a random member from the list provided in `members`.
-
- The invoking author is ignored.
- """
- if author in members:
- members.remove(author)
-
- return random.choice(members) if members else None
-
- @staticmethod
def random_emoji() -> Tuple[str, str]:
"""Return two random emoji from the module-defined constants."""
emoji_1 = random.choice(HEART_EMOJIS)
diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py
index c75ea6cf..966acc82 100644
--- a/bot/exts/valentines/lovecalculator.py
+++ b/bot/exts/valentines/lovecalculator.py
@@ -4,15 +4,13 @@ import json
import logging
import random
from pathlib import Path
-from typing import Union
+from typing import Coroutine, Union
import discord
from discord import Member
from discord.ext import commands
from discord.ext.commands import BadArgument, Cog, clean_content
-from bot.constants import Roles
-
log = logging.getLogger(__name__)
with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file:
@@ -46,14 +44,11 @@ class LoveCalculator(Cog):
If you want to use multiple words for one argument, you must include quotes.
.love "Zes Vappa" "morning coffee"
-
- If only one argument is provided, the subject will become one of the helpers at random.
"""
if whom is None:
- staff = ctx.guild.get_role(Roles.helpers).members
- whom = random.choice(staff)
+ whom = ctx.author
- def normalize(arg: Union[Member, str]) -> str:
+ def normalize(arg: Union[Member, str]) -> Coroutine:
if isinstance(arg, Member):
# If we are given a member, return name#discrim without any extra changes
arg = str(arg)
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 9cdaad3f..c12a15ff 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -13,6 +13,7 @@ from discord.ext.commands import CheckFailure, Command, Context
from bot.constants import ERROR_REPLIES, Month
from bot.utils import human_months, resolve_current_month
+from bot.utils.checks import in_whitelist_check
ONE_DAY = 24 * 60 * 60
@@ -186,82 +187,104 @@ def without_role(*role_ids: int) -> t.Callable:
return commands.check(predicate)
-def in_channel_check(*channels: int, bypass_roles: t.Container[int] = None) -> t.Callable[[Context], bool]:
+def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context], bool]:
"""
- Checks that the message is in a whitelisted channel or optionally has a bypass role.
+ Checks if a message is sent in a whitelisted context.
- If `in_channel_override` is present, check if it contains channels
- and use them in place of the global whitelist.
+ All arguments from `in_whitelist_check` are supported, with the exception of "fail_silently".
+ If `whitelist_override` is present, it is added to the global whitelist.
"""
def predicate(ctx: Context) -> bool:
+ # Skip DM invocations
if not ctx.guild:
log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.")
return True
- if ctx.channel.id in channels:
- log.debug(
- f"{ctx.author} tried to call the '{ctx.command.name}' command "
- f"and the command was used in a whitelisted channel."
- )
- return True
- if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles):
- log.debug(
- f"{ctx.author} called the '{ctx.command.name}' command and "
- f"had a role to bypass the in_channel check."
- )
- return True
+ kwargs = default_kwargs.copy()
- if hasattr(ctx.command.callback, "in_channel_override"):
- override = ctx.command.callback.in_channel_override
- if override is None:
+ # Update kwargs based on override
+ if hasattr(ctx.command.callback, "override"):
+ # Remove default kwargs if reset is True
+ if ctx.command.callback.override_reset:
+ kwargs = {}
log.debug(
- f"{ctx.author} called the '{ctx.command.name}' command "
- f"and the command was whitelisted to bypass the in_channel check."
+ f"{ctx.author} called the '{ctx.command.name}' command and "
+ f"overrode default checks."
)
- return True
- else:
- if ctx.channel.id in override:
- log.debug(
- f"{ctx.author} tried to call the '{ctx.command.name}' command "
- f"and the command was used in an overridden whitelisted channel."
- )
- return True
- log.debug(
- f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The overridden in_channel check failed."
- )
- channels_str = ', '.join(f"<#{c_id}>" for c_id in override)
- raise InChannelCheckFailure(
- f"Sorry, but you may only use this command within {channels_str}."
- )
+ # Merge overwrites and defaults
+ for arg in ctx.command.callback.override:
+ default_value = kwargs.get(arg)
+ new_value = ctx.command.callback.override[arg]
+
+ # Skip values that don't need merging, or can't be merged
+ if default_value is None or isinstance(arg, int):
+ kwargs[arg] = new_value
+
+ # Merge containers
+ elif isinstance(default_value, t.Container):
+ if isinstance(new_value, t.Container):
+ kwargs[arg] = (*default_value, *new_value)
+ else:
+ kwargs[arg] = new_value
+
+ log.debug(
+ f"Updated default check arguments for '{ctx.command.name}' "
+ f"invoked by {ctx.author}."
+ )
+
+ log.trace(f"Calling whitelist check for {ctx.author} for command {ctx.command.name}.")
+ result = in_whitelist_check(ctx, fail_silently=True, **kwargs)
+
+ # Return if check passed
+ if result:
+ log.debug(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command "
+ f"and the command was used in an overridden context."
+ )
+ return result
log.debug(
f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The in_channel check failed."
+ f"The whitelist check failed."
)
- channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
- raise InChannelCheckFailure(
- f"Sorry, but you may only use this command within {channels_str}."
- )
+ # Raise error if the check did not pass
+ channels = set(kwargs.get("channels") or {})
+ categories = kwargs.get("categories")
- return predicate
+ # Add all whitelisted category channels
+ if categories:
+ for category_id in categories:
+ category = ctx.guild.get_channel(category_id)
+ if category is None:
+ continue
+ [channels.add(channel.id) for channel in category.text_channels]
-in_channel = commands.check(in_channel_check)
+ if channels:
+ channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
+ message = f"Sorry, but you may only use this command within {channels_str}."
+ else:
+ message = "Sorry, but you may not use this command."
+
+ raise InChannelCheckFailure(message)
+
+ return predicate
-def override_in_channel(channels: t.Tuple[int] = None) -> t.Callable:
+def whitelist_override(bypass_defaults: bool = False, **kwargs: t.Container[int]) -> t.Callable:
"""
- Set command callback attribute for detection in `in_channel_check`.
+ Override global whitelist context, with the kwargs specified.
- Override global whitelist if channels are specified.
+ All arguments from `in_whitelist_check` are supported, with the exception of `fail_silently`.
+ Set `bypass_defaults` to True if you want to completely bypass global checks.
This decorator has to go before (below) below the `command` decorator.
"""
def inner(func: t.Callable) -> t.Callable:
- func.in_channel_override = channels
+ func.override = kwargs
+ func.override_reset = bypass_defaults
return func
return inner