aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar MrKomodoDragon <[email protected]>2021-03-04 14:35:30 -0800
committerGravatar GitHub <[email protected]>2021-03-04 14:35:30 -0800
commit431465c27cc0aa1ac939c777680dc5cd8062b330 (patch)
treeb96918fd6db523eaa2807f60e73da61478faf23e
parentRemove "Want to suggest a fact?" for consistency (diff)
parentMerge pull request #609 from Kronifer/earth_photos (diff)
Merge branch 'master' into master
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock21
-rw-r--r--bot/bot.py17
-rw-r--r--bot/constants.py16
-rw-r--r--bot/exts/easter/earth_photos.py61
-rw-r--r--bot/exts/evergreen/connect_four.py450
-rw-r--r--bot/exts/evergreen/error_handler.py9
-rw-r--r--bot/exts/evergreen/issues.py6
-rw-r--r--bot/exts/evergreen/wikipedia.py164
-rw-r--r--bot/resources/evergreen/py_topics.yaml53
-rw-r--r--bot/resources/evergreen/starter.yaml11
11 files changed, 691 insertions, 118 deletions
diff --git a/Pipfile b/Pipfile
index c382902f..e7e01a31 100644
--- a/Pipfile
+++ b/Pipfile
@@ -14,6 +14,7 @@ sentry-sdk = "~=0.19"
PyYAML = "~=5.3.1"
"discord.py" = {extras = ["voice"], version = "~=1.5.1"}
async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"}
+emojis = "~=0.6.0"
[dev-packages]
flake8 = "~=3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
index bd894ffa..ec801979 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "9be419062bd9db364ac9dddfcd50aef9c932384b45850363e482591fe7d12403"
+ "sha256": "b4aaaacbab13179145e36d7b86c736db512286f6cce8e513cc30c48d68fe3810"
},
"pipfile-spec": 6,
"requires": {
@@ -161,6 +161,14 @@
"index": "pypi",
"version": "==1.5.1"
},
+ "emojis": {
+ "hashes": [
+ "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c",
+ "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"
+ ],
+ "index": "pypi",
+ "version": "==0.6.0"
+ },
"fakeredis": {
"hashes": [
"sha256:01cb47d2286825a171fb49c0e445b1fa9307087e07cbb3d027ea10dbff108b6a",
@@ -406,10 +414,11 @@
},
"sentry-sdk": {
"hashes": [
- "sha256:3693cb47ba8d90c004ac002425770b32aaf0c83a846ec48e2d1364e7db1d072d"
+ "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237",
+ "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"
],
"index": "pypi",
- "version": "==0.20.1"
+ "version": "==0.20.3"
},
"six": {
"hashes": [
@@ -576,11 +585,11 @@
},
"identify": {
"hashes": [
- "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66",
- "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4"
+ "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc",
+ "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.5.13"
+ "version": "==1.5.14"
},
"mccabe": {
"hashes": [
diff --git a/bot/bot.py b/bot/bot.py
index 97b09243..e9750697 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -34,7 +34,7 @@ class Bot(commands.Bot):
)
self._guild_available = asyncio.Event()
self.redis_session = redis_session
-
+ self.loop.create_task(self.check_channels())
self.loop.create_task(self.send_log(self.name, "Connected!"))
@property
@@ -71,6 +71,21 @@ class Bot(commands.Bot):
else:
await super().on_command_error(context, exception)
+ async def check_channels(self) -> None:
+ """Verifies that all channel constants refer to channels which exist."""
+ await self.wait_until_guild_available()
+
+ if constants.Client.debug:
+ log.info("Skipping Channels Check.")
+ return
+
+ all_channels_ids = [channel.id for channel in self.get_all_channels()]
+ for name, channel_id in vars(constants.Channels).items():
+ if name.startswith('_'):
+ continue
+ if channel_id not in all_channels_ids:
+ log.error(f'Channel "{name}" with ID {channel_id} missing')
+
async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None:
"""Send an embed message to the devlog channel."""
await self.wait_until_guild_available()
diff --git a/bot/constants.py b/bot/constants.py
index 3aec6ba3..f6e09ae7 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -124,6 +124,7 @@ class Channels(NamedTuple):
hacktoberfest_2020 = 760857070781071431
voice_chat_0 = 412357430186344448
voice_chat_1 = 799647045886541885
+ staff_voice = 541638762007101470
class Categories(NamedTuple):
@@ -131,6 +132,7 @@ class Categories(NamedTuple):
development = 411199786025484308
devprojects = 787641585624940544
media = 799054581991997460
+ staff = 364918151625965579
class Client(NamedTuple):
@@ -156,8 +158,12 @@ class Colours:
soft_orange = 0xf9cb54
soft_red = 0xcd6d6d
yellow = 0xf9f586
+<<<<<<< master
python_blue = 0x4B8BBE
python_yellow = 0xFFD43B
+=======
+ grass_green = 0x66ff00
+>>>>>>> master
class Emojis:
@@ -167,6 +173,7 @@ class Emojis:
envelope = "\U0001F4E8"
trashcan = "<:trashcan:637136429717389331>"
ok_hand = ":ok_hand:"
+ hand_raised = "\U0001f64b"
dice_1 = "<:dice_1:755891608859443290>"
dice_2 = "<:dice_2:755891608741740635>"
@@ -181,7 +188,6 @@ class Emojis:
pull_request_closed = "<:PRClosed:629695470519713818>"
merge = "<:PRMerged:629695470570176522>"
- # TicTacToe Emojis
number_emojis = {
1: "\u0031\ufe0f\u20e3",
2: "\u0032\ufe0f\u20e3",
@@ -193,8 +199,11 @@ class Emojis:
8: "\u0038\ufe0f\u20e3",
9: "\u0039\ufe0f\u20e3"
}
+
confirmation = "\u2705"
decline = "\u274c"
+ incident_unactioned = "<:incident_unactioned:719645583245180960>"
+
x = "\U0001f1fd"
o = "\U0001f1f4"
@@ -268,6 +277,7 @@ class Tokens(NamedTuple):
igdb_client_id = environ.get("IGDB_CLIENT_ID")
igdb_client_secret = environ.get("IGDB_CLIENT_SECRET")
github = environ.get("GITHUB_TOKEN")
+ unsplash_access_key = environ.get("UNSPLASH_KEY")
class Wolfram(NamedTuple):
@@ -283,10 +293,6 @@ class RedisConfig(NamedTuple):
use_fakeredis = environ.get("USE_FAKEREDIS", "false").lower() == "true"
-class Wikipedia:
- total_chance = 3
-
-
class Source:
github = "https://github.com/python-discord/sir-lancebot"
github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
diff --git a/bot/exts/easter/earth_photos.py b/bot/exts/easter/earth_photos.py
new file mode 100644
index 00000000..60e34b15
--- /dev/null
+++ b/bot/exts/easter/earth_photos.py
@@ -0,0 +1,61 @@
+import logging
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+from bot.constants import Tokens
+
+log = logging.getLogger(__name__)
+
+
+class EarthPhotos(commands.Cog):
+ """This cog contains the command for earth photos."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.command(aliases=["earth"])
+ async def earth_photos(self, ctx: commands.Context) -> None:
+ """Returns a random photo of earth, sourced from Unsplash."""
+ async with ctx.typing():
+ async with self.bot.http_session.get(
+ 'https://api.unsplash.com/photos/random',
+ params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key}
+ ) as r:
+ jsondata = await r.json()
+ linksdata = jsondata.get("urls")
+ embedlink = linksdata.get("regular")
+ downloadlinksdata = jsondata.get("links")
+ userdata = jsondata.get("user")
+ username = userdata.get("name")
+ userlinks = userdata.get("links")
+ profile = userlinks.get("html")
+ # Referral flags
+ rf = "?utm_source=Sir%20Lancebot&utm_medium=referral"
+ async with self.bot.http_session.get(
+ downloadlinksdata.get("download_location"),
+ params={"client_id": Tokens.unsplash_access_key}
+ ) as _:
+ pass
+
+ embed = discord.Embed(
+ title="Earth Photo",
+ description="A photo of Earth 🌎 from Unsplash.",
+ color=Colours.grass_green
+ )
+ embed.set_image(url=embedlink)
+ embed.add_field(
+ name="Author",
+ value=f"Photo by [{username}]({profile}{rf}) \
+ on [Unsplash](https://unsplash.com{rf})."
+ )
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the Earth Photos cog."""
+ if not Tokens.unsplash_access_key:
+ log.warning("No Unsplash access key found. Cog not loading.")
+ return
+ bot.add_cog(EarthPhotos(bot))
diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py
new file mode 100644
index 00000000..7e3ec42b
--- /dev/null
+++ b/bot/exts/evergreen/connect_four.py
@@ -0,0 +1,450 @@
+import asyncio
+import random
+import typing
+from functools import partial
+
+import discord
+import emojis
+from discord.ext import commands
+from discord.ext.commands import guild_only
+
+from bot.constants import Emojis
+
+NUMBERS = list(Emojis.number_emojis.values())
+CROSS_EMOJI = Emojis.incident_unactioned
+
+Coordinate = typing.Optional[typing.Tuple[int, int]]
+EMOJI_CHECK = typing.Union[discord.Emoji, str]
+
+
+class Game:
+ """A Connect 4 Game."""
+
+ def __init__(
+ self,
+ bot: commands.Bot,
+ channel: discord.TextChannel,
+ player1: discord.Member,
+ player2: typing.Optional[discord.Member],
+ tokens: typing.List[str],
+ size: int = 7
+ ) -> None:
+
+ self.bot = bot
+ self.channel = channel
+ self.player1 = player1
+ self.player2 = player2 or AI(self.bot, game=self)
+ self.tokens = tokens
+
+ self.grid = self.generate_board(size)
+ self.grid_size = size
+
+ self.unicode_numbers = NUMBERS[:self.grid_size]
+
+ self.message = None
+
+ self.player_active = None
+ self.player_inactive = None
+
+ @staticmethod
+ def generate_board(size: int) -> typing.List[typing.List[int]]:
+ """Generate the connect 4 board."""
+ return [[0 for _ in range(size)] for _ in range(size)]
+
+ async def print_grid(self) -> None:
+ """Formats and outputs the Connect Four grid to the channel."""
+ title = (
+ f'Connect 4: {self.player1.display_name}'
+ f' VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}'
+ )
+
+ rows = [" ".join(self.tokens[s] for s in row) for row in self.grid]
+ first_row = " ".join(x for x in NUMBERS[:self.grid_size])
+ formatted_grid = "\n".join([first_row] + rows)
+ embed = discord.Embed(title=title, description=formatted_grid)
+
+ if self.message:
+ await self.message.edit(embed=embed)
+ else:
+ self.message = await self.channel.send(content='Loading...')
+ for emoji in self.unicode_numbers:
+ await self.message.add_reaction(emoji)
+ await self.message.add_reaction(CROSS_EMOJI)
+ await self.message.edit(content=None, embed=embed)
+
+ async def game_over(self, action: str, player1: discord.user, player2: discord.user) -> None:
+ """Announces to public chat."""
+ if action == "win":
+ await self.channel.send(f"Game Over! {player1.mention} won against {player2.mention}")
+ elif action == "draw":
+ await self.channel.send(f"Game Over! {player1.mention} {player2.mention} It's A Draw :tada:")
+ elif action == "quit":
+ await self.channel.send(f"{self.player1.mention} surrendered. Game over!")
+ await self.print_grid()
+
+ async def start_game(self) -> None:
+ """Begins the game."""
+ self.player_active, self.player_inactive = self.player1, self.player2
+
+ while True:
+ await self.print_grid()
+
+ if isinstance(self.player_active, AI):
+ coords = self.player_active.play()
+ if not coords:
+ await self.game_over(
+ "draw",
+ self.bot.user if isinstance(self.player_active, AI) else self.player_active,
+ self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive,
+ )
+ else:
+ coords = await self.player_turn()
+
+ if not coords:
+ return
+
+ if self.check_win(coords, 1 if self.player_active == self.player1 else 2):
+ await self.game_over(
+ "win",
+ self.bot.user if isinstance(self.player_active, AI) else self.player_active,
+ self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive,
+ )
+ return
+
+ self.player_active, self.player_inactive = self.player_inactive, self.player_active
+
+ def predicate(self, reaction: discord.Reaction, user: discord.Member) -> bool:
+ """The predicate to check for the player's reaction."""
+ return (
+ reaction.message.id == self.message.id
+ and user.id == self.player_active.id
+ and str(reaction.emoji) in (*self.unicode_numbers, CROSS_EMOJI)
+ )
+
+ async def player_turn(self) -> Coordinate:
+ """Initiate the player's turn."""
+ message = await self.channel.send(
+ f"{self.player_active.mention}, it's your turn! React with the column you want to place your token in."
+ )
+ player_num = 1 if self.player_active == self.player1 else 2
+ while True:
+ try:
+ reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0)
+ except asyncio.TimeoutError:
+ await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!")
+ return
+ else:
+ await message.delete()
+ if str(reaction.emoji) == CROSS_EMOJI:
+ await self.game_over("quit", self.player_active, self.player_inactive)
+ return
+
+ await self.message.remove_reaction(reaction, user)
+
+ column_num = self.unicode_numbers.index(str(reaction.emoji))
+ column = [row[column_num] for row in self.grid]
+
+ for row_num, square in reversed(list(enumerate(column))):
+ if not square:
+ self.grid[row_num][column_num] = player_num
+ return row_num, column_num
+ message = await self.channel.send(f"Column {column_num + 1} is full. Try again")
+
+ def check_win(self, coords: Coordinate, player_num: int) -> bool:
+ """Check that placing a counter here would cause the player to win."""
+ vertical = [(-1, 0), (1, 0)]
+ horizontal = [(0, 1), (0, -1)]
+ forward_diag = [(-1, 1), (1, -1)]
+ backward_diag = [(-1, -1), (1, 1)]
+ axes = [vertical, horizontal, forward_diag, backward_diag]
+
+ for axis in axes:
+ counters_in_a_row = 1 # The initial counter that is compared to
+ for (row_incr, column_incr) in axis:
+ row, column = coords
+ row += row_incr
+ column += column_incr
+
+ while 0 <= row < self.grid_size and 0 <= column < self.grid_size:
+ if self.grid[row][column] == player_num:
+ counters_in_a_row += 1
+ row += row_incr
+ column += column_incr
+ else:
+ break
+ if counters_in_a_row >= 4:
+ return True
+ return False
+
+
+class AI:
+ """The Computer Player for Single-Player games."""
+
+ def __init__(self, bot: commands.Bot, game: Game) -> None:
+ self.game = game
+ self.mention = bot.user.mention
+
+ def get_possible_places(self) -> typing.List[Coordinate]:
+ """Gets all the coordinates where the AI could possibly place a counter."""
+ possible_coords = []
+ for column_num in range(self.game.grid_size):
+ column = [row[column_num] for row in self.game.grid]
+ for row_num, square in reversed(list(enumerate(column))):
+ if not square:
+ possible_coords.append((row_num, column_num))
+ break
+ return possible_coords
+
+ def check_ai_win(self, coord_list: typing.List[Coordinate]) -> typing.Optional[Coordinate]:
+ """
+ Check AI win.
+
+ Check if placing a counter in any possible coordinate would cause the AI to win
+ with 10% chance of not winning and returning None
+ """
+ if random.randint(1, 10) == 1:
+ return
+ for coords in coord_list:
+ if self.game.check_win(coords, 2):
+ return coords
+
+ def check_player_win(self, coord_list: typing.List[Coordinate]) -> typing.Optional[Coordinate]:
+ """
+ Check Player win.
+
+ Check if placing a counter in possible coordinates would stop the player
+ from winning with 25% of not blocking them and returning None.
+ """
+ if random.randint(1, 4) == 1:
+ return
+ for coords in coord_list:
+ if self.game.check_win(coords, 1):
+ return coords
+
+ @staticmethod
+ def random_coords(coord_list: typing.List[Coordinate]) -> Coordinate:
+ """Picks a random coordinate from the possible ones."""
+ return random.choice(coord_list)
+
+ def play(self) -> typing.Union[Coordinate, bool]:
+ """
+ Plays for the AI.
+
+ Gets all possible coords, and determins the move:
+ 1. coords where it can win.
+ 2. coords where the player can win.
+ 3. Random coord
+ The first possible value is choosen.
+ """
+ possible_coords = self.get_possible_places()
+
+ if not possible_coords:
+ return False
+
+ coords = (
+ self.check_ai_win(possible_coords)
+ or self.check_player_win(possible_coords)
+ or self.random_coords(possible_coords)
+ )
+
+ row, column = coords
+ self.game.grid[row][column] = 2
+ return coords
+
+
+class ConnectFour(commands.Cog):
+ """Connect Four. The Classic Vertical Four-in-a-row Game!"""
+
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+ self.games: typing.List[Game] = []
+ self.waiting: typing.List[discord.Member] = []
+
+ self.tokens = [":white_circle:", ":blue_circle:", ":red_circle:"]
+
+ self.max_board_size = 9
+ self.min_board_size = 5
+
+ async def check_author(self, ctx: commands.Context, board_size: int) -> bool:
+ """Check if the requester is free and the board size is correct."""
+ if self.already_playing(ctx.author):
+ await ctx.send("You're already playing a game!")
+ return False
+
+ if ctx.author in self.waiting:
+ await ctx.send("You've already sent out a request for a player 2")
+ return False
+
+ if not self.min_board_size <= board_size <= self.max_board_size:
+ await ctx.send(f"{board_size} is not a valid board size. A valid board size is "
+ f"between `{self.min_board_size}` and `{self.max_board_size}`.")
+ return False
+
+ return True
+
+ def get_player(
+ self,
+ ctx: commands.Context,
+ announcement: discord.Message,
+ reaction: discord.Reaction,
+ user: discord.Member
+ ) -> bool:
+ """Predicate checking the criteria for the announcement message."""
+ if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2
+ return True # Is dealt with later on
+
+ if (
+ user.id not in (ctx.me.id, ctx.author.id)
+ and str(reaction.emoji) == Emojis.hand_raised
+ and reaction.message.id == announcement.id
+ ):
+ if self.already_playing(user):
+ self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!"))
+ self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
+ return False
+
+ if user in self.waiting:
+ self.bot.loop.create_task(ctx.send(
+ f"{user.mention} Please cancel your game first before joining another one."
+ ))
+ self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
+ return False
+
+ return True
+
+ if (
+ user.id == ctx.author.id
+ and str(reaction.emoji) == CROSS_EMOJI
+ and reaction.message.id == announcement.id
+ ):
+ return True
+ return False
+
+ def already_playing(self, player: discord.Member) -> bool:
+ """Check if someone is already in a game."""
+ return any(player in (game.player1, game.player2) for game in self.games)
+
+ @staticmethod
+ def check_emojis(
+ e1: EMOJI_CHECK, e2: EMOJI_CHECK
+ ) -> typing.Tuple[bool, typing.Optional[str]]:
+ """Validate the emojis, the user put."""
+ if isinstance(e1, str) and emojis.count(e1) != 1:
+ return False, e1
+ if isinstance(e2, str) and emojis.count(e2) != 1:
+ return False, e2
+ return True, None
+
+ async def _play_game(
+ self,
+ ctx: commands.Context,
+ user: typing.Optional[discord.Member],
+ board_size: int,
+ emoji1: str,
+ emoji2: str
+ ) -> None:
+ """Helper for playing a game of connect four."""
+ self.tokens = [":white_circle:", str(emoji1), str(emoji2)]
+ game = None # if game fails to intialize in try...except
+
+ try:
+ game = Game(self.bot, ctx.channel, ctx.author, user, self.tokens, size=board_size)
+ self.games.append(game)
+ await game.start_game()
+ self.games.remove(game)
+ except Exception:
+ # End the game in the event of an unforeseen error so the players aren't stuck in a game
+ await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed")
+ if game in self.games:
+ self.games.remove(game)
+ raise
+
+ @guild_only()
+ @commands.group(
+ invoke_without_command=True,
+ aliases=["4inarow", "connect4", "connectfour", "c4"],
+ case_insensitive=True
+ )
+ async def connect_four(
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
+ ) -> None:
+ """
+ Play the classic game of Connect Four with someone!
+
+ Sets up a message waiting for someone else to react and play along.
+ The game will start once someone has reacted.
+ All inputs will be through reactions.
+ """
+ check, emoji = self.check_emojis(emoji1, emoji2)
+ if not check:
+ raise commands.EmojiNotFound(emoji)
+
+ check_author_result = await self.check_author(ctx, board_size)
+ if not check_author_result:
+ return
+
+ announcement = await ctx.send(
+ "**Connect Four**: A new game is about to start!\n"
+ f"Press {Emojis.hand_raised} to play against {ctx.author.mention}!\n"
+ f"(Cancel the game with {CROSS_EMOJI}.)"
+ )
+ self.waiting.append(ctx.author)
+ await announcement.add_reaction(Emojis.hand_raised)
+ await announcement.add_reaction(CROSS_EMOJI)
+
+ try:
+ reaction, user = await self.bot.wait_for(
+ "reaction_add",
+ check=partial(self.get_player, ctx, announcement),
+ timeout=60.0
+ )
+ except asyncio.TimeoutError:
+ self.waiting.remove(ctx.author)
+ await announcement.delete()
+ await ctx.send(
+ f"{ctx.author.mention} Seems like there's no one here to play. "
+ f"Use `{ctx.prefix}{ctx.invoked_with} ai` to play against a computer."
+ )
+ return
+
+ if str(reaction.emoji) == CROSS_EMOJI:
+ self.waiting.remove(ctx.author)
+ await announcement.delete()
+ await ctx.send(f"{ctx.author.mention} Game cancelled.")
+ return
+
+ await announcement.delete()
+ self.waiting.remove(ctx.author)
+ if self.already_playing(ctx.author):
+ return
+
+ await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2))
+
+ @guild_only()
+ @connect_four.command(aliases=["bot", "computer", "cpu"])
+ async def ai(
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
+ ) -> None:
+ """Play Connect Four against a computer player."""
+ check, emoji = self.check_emojis(emoji1, emoji2)
+ if not check:
+ raise commands.EmojiNotFound(emoji)
+
+ check_author_result = await self.check_author(ctx, board_size)
+ if not check_author_result:
+ return
+
+ await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2))
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load ConnectFour Cog."""
+ bot.add_cog(ConnectFour(bot))
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 99af1519..28902503 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -7,7 +7,7 @@ from discord import Embed, Message
from discord.ext import commands
from sentry_sdk import push_scope
-from bot.constants import Colours, ERROR_REPLIES, NEGATIVE_REPLIES
+from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import UserNotPlayingError
@@ -83,7 +83,12 @@ class CommandErrorHandler(commands.Cog):
return
if isinstance(error, commands.NoPrivateMessage):
- await ctx.send(embed=self.error_embed("This command can only be used in the server.", NEGATIVE_REPLIES))
+ await ctx.send(
+ embed=self.error_embed(
+ f"This command can only be used in the server. Go to <#{Channels.community_bot_commands}> instead!",
+ NEGATIVE_REPLIES
+ )
+ )
return
if isinstance(error, commands.BadArgument):
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index 73ebe547..bbcbf611 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -24,9 +24,11 @@ if GITHUB_TOKEN := Tokens.github:
REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
WHITELISTED_CATEGORIES = (
- Categories.devprojects, Categories.media, Categories.development
+ Categories.development, Categories.devprojects, Categories.media, Categories.staff
+)
+WHITELISTED_CHANNELS_ON_MESSAGE = (
+ Channels.organisation, Channels.mod_meta, Channels.mod_tools, Channels.staff_voice
)
-WHITELISTED_CHANNELS_ON_MESSAGE = (Channels.organisation, Channels.mod_meta, Channels.mod_tools)
CODE_BLOCK_RE = re.compile(
r"^`([^`\n]+)`" # Inline codeblock
diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py
index be36e2c4..068c4f43 100644
--- a/bot/exts/evergreen/wikipedia.py
+++ b/bot/exts/evergreen/wikipedia.py
@@ -1,114 +1,94 @@
-import asyncio
-import datetime
import logging
+import re
+from datetime import datetime
+from html import unescape
from typing import List, Optional
-from aiohttp import client_exceptions
-from discord import Color, Embed, Message
+from discord import Color, Embed, TextChannel
from discord.ext import commands
-from bot.constants import Wikipedia
+from bot.bot import Bot
+from bot.utils import LinePaginator
log = logging.getLogger(__name__)
-SEARCH_API = "https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={search_term}&format=json"
-WIKIPEDIA_URL = "https://en.wikipedia.org/wiki/{title}"
+SEARCH_API = (
+ "https://en.wikipedia.org/w/api.php?action=query&list=search&prop=info&inprop=url&utf8=&"
+ "format=json&origin=*&srlimit={number_of_results}&srsearch={string}"
+)
+WIKI_THUMBNAIL = (
+ "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg"
+ "/330px-Wikipedia-logo-v2.svg.png"
+)
+WIKI_SNIPPET_REGEX = r'(<!--.*?-->|<[^>]*>)'
+WIKI_SEARCH_RESULT = (
+ "**[{name}]({url})**\n"
+ "{description}\n"
+)
class WikipediaSearch(commands.Cog):
"""Get info from wikipedia."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- self.http_session = bot.http_session
- @staticmethod
- def formatted_wiki_url(index: int, title: str) -> str:
- """Formating wikipedia link with index and title."""
- return f'`{index}` [{title}]({WIKIPEDIA_URL.format(title=title.replace(" ", "_"))})'
+ async def wiki_request(self, channel: TextChannel, search: str) -> Optional[List[str]]:
+ """Search wikipedia search string and return formatted first 10 pages found."""
+ url = SEARCH_API.format(number_of_results=10, string=search)
+ async with self.bot.http_session.get(url=url) as resp:
+ if resp.status == 200:
+ raw_data = await resp.json()
+ number_of_results = raw_data['query']['searchinfo']['totalhits']
+
+ if number_of_results:
+ results = raw_data['query']['search']
+ lines = []
+
+ for article in results:
+ line = WIKI_SEARCH_RESULT.format(
+ name=article['title'],
+ description=unescape(
+ re.sub(
+ WIKI_SNIPPET_REGEX, '', article['snippet']
+ )
+ ),
+ url=f"https://en.wikipedia.org/?curid={article['pageid']}"
+ )
+ lines.append(line)
+
+ return lines
- async def search_wikipedia(self, search_term: str) -> Optional[List[str]]:
- """Search wikipedia and return the first 10 pages found."""
- pages = []
- async with self.http_session.get(SEARCH_API.format(search_term=search_term)) as response:
- try:
- data = await response.json()
-
- search_results = data["query"]["search"]
-
- # Ignore pages with "may refer to"
- for search_result in search_results:
- log.info("trying to append titles")
- if "may refer to" not in search_result["snippet"]:
- pages.append(search_result["title"])
- except client_exceptions.ContentTypeError:
- pages = None
-
- log.info("Finished appending titles")
- return pages
+ else:
+ await channel.send(
+ "Sorry, we could not find a wikipedia article using that search term."
+ )
+ return
+ else:
+ log.info(f"Unexpected response `{resp.status}` while searching wikipedia for `{search}`")
+ await channel.send(
+ "Whoops, the Wikipedia API is having some issues right now. Try again later."
+ )
+ return
@commands.cooldown(1, 10, commands.BucketType.user)
@commands.command(name="wikipedia", aliases=["wiki"])
async def wikipedia_search_command(self, ctx: commands.Context, *, search: str) -> None:
- """Return list of results containing your search query from wikipedia."""
- titles = await self.search_wikipedia(search)
-
- def check(message: Message) -> bool:
- return message.author.id == ctx.author.id and message.channel == ctx.channel
-
- if not titles:
- await ctx.send("Sorry, we could not find a wikipedia article using that search term")
- return
-
- async with ctx.typing():
- log.info("Finished appending titles to titles_no_underscore list")
-
- s_desc = "\n".join(self.formatted_wiki_url(index, title) for index, title in enumerate(titles, start=1))
- embed = Embed(colour=Color.blue(), title=f"Wikipedia results for `{search}`", description=s_desc)
- embed.timestamp = datetime.datetime.utcnow()
- await ctx.send(embed=embed)
- embed = Embed(colour=Color.green(), description="Enter number to choose")
- msg = await ctx.send(embed=embed)
- titles_len = len(titles) # getting length of list
-
- for retry_count in range(1, Wikipedia.total_chance + 1):
- retries_left = Wikipedia.total_chance - retry_count
- if retry_count < Wikipedia.total_chance:
- error_msg = f"You have `{retries_left}/{Wikipedia.total_chance}` chances left"
- else:
- error_msg = 'Please try again by using `.wiki` command'
- try:
- message = await ctx.bot.wait_for('message', timeout=60.0, check=check)
- response_from_user = await self.bot.get_context(message)
-
- if response_from_user.command:
- return
-
- response = int(message.content)
- if response < 0:
- await ctx.send(f"Sorry, but you can't give negative index, {error_msg}")
- elif response == 0:
- await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}")
- else:
- await ctx.send(WIKIPEDIA_URL.format(title=titles[response - 1].replace(" ", "_")))
- break
-
- except asyncio.TimeoutError:
- embed = Embed(colour=Color.red(), description=f"Time's up {ctx.author.mention}")
- await msg.edit(embed=embed)
- break
-
- except ValueError:
- await ctx.send(f"Sorry, but you cannot do that, I will only accept an positive integer, {error_msg}")
-
- except IndexError:
- await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}")
-
- except Exception as e:
- log.info(f"Caught exception {e}, breaking out of retry loop")
- break
-
-
-def setup(bot: commands.Bot) -> None:
+ """Sends paginated top 10 results of Wikipedia search.."""
+ contents = await self.wiki_request(ctx.channel, search)
+
+ if contents:
+ embed = Embed(
+ title="Wikipedia Search Results",
+ colour=Color.blurple()
+ )
+ embed.set_thumbnail(url=WIKI_THUMBNAIL)
+ embed.timestamp = datetime.utcnow()
+ await LinePaginator.paginate(
+ contents, ctx, embed
+ )
+
+
+def setup(bot: Bot) -> None:
"""Wikipedia Cog load."""
bot.add_cog(WikipediaSearch(bot))
diff --git a/bot/resources/evergreen/py_topics.yaml b/bot/resources/evergreen/py_topics.yaml
index 1e53429a..f3b2eaa3 100644
--- a/bot/resources/evergreen/py_topics.yaml
+++ b/bot/resources/evergreen/py_topics.yaml
@@ -3,8 +3,6 @@
# python-general
267624335836053506:
- What's your favorite PEP?
- - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python?
- - What functionality is your text editor/IDE missing for programming Python?
- What parts of your life has Python automated, if any?
- Which Python project are you the most proud of making?
- What made you want to learn Python?
@@ -16,23 +14,34 @@
- What feature do you think should be added to Python?
- Has Python helped you in school? If so, how?
- What was the first thing you created with Python?
+ - What is your favorite Python package?
+ - What standard library module is really underrated?
+ - Have you published any packages on PyPi? If so, what are they?
+ - What are you currently working on in Python?
+ - What's your favorite script and how has it helped you in day to day activities?
+ - When you were first learning, what is something that stumped you?
+ - When you were first learning, what is a resource you wish you had?
+ - What is something you know now, that you wish you knew when starting out?
+ - What is something simple that you still error on today?
+
+# algos-and-data-structs
+650401909852864553:
+ -
# async
630504881542791169:
- Are there any frameworks you wish were async?
- How have coroutines changed the way you write Python?
+ - What is your favorite async library?
# c-extensions
728390945384431688:
-
-# computer-science
-650401909852864553:
- -
-
# databases
342318764227821568:
- Where do you get your best data?
+ - What is your preferred database and for what use?
# data-science
366673247892275221:
@@ -45,11 +54,18 @@
- What feature would you be the most interested in making?
- What feature would you like to see added to the library? what feature in the library do you think is redundant?
- Do you think there's a way in which Discord could handle bots better?
+ - What's one feature you wish more developers had in their bots?
+
+# editors-ides
+813178633006350366:
+ - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python?
+ - What functionality is your text editor/IDE missing for programming Python?
# esoteric-python
470884583684964352:
- What's a common part of programming we can make harder?
- What are the pros and cons of messing with __magic__()?
+ - What's your favorite Python hack?
# game-development
660625198390837248:
@@ -57,7 +73,7 @@
# microcontrollers
545603026732318730:
- -
+ - What is your favorite version of the Raspberry Pi?
# networking
716325106619777044:
@@ -67,23 +83,40 @@
366674035876167691:
- If you could wish for a library involving net-sec, what would it be?
-# software-testing
-463035728335732738:
+# software-design
+782713858615017503:
-
# tools-and-devops
463035462760792066:
- What editor would you recommend to a beginner? Why?
- What editor would you recommend to be the most efficient? Why?
+ - How often do you use GitHub Actions and workflows to automate your repositories?
+ - What's your favorite app on GitHub?
+
+# unit-testing
+463035728335732738:
+ -
# unix
491523972836360192:
- -
+ - What's your favorite Bash command?
+ - What's your most used Bash command?
+ - How often do you update your Unix machine?
+ - How often do you upgrade on production?
# user-interfaces
338993628049571840:
- What's the most impressive Desktop Application you've made with Python so far?
+ - Have you ever made your own GUI? If so, how?
+ - Do you perfer Command Line Interfaces (CLI) or Graphic User Interfaces (GUI)?
+ - What's your favorite CLI (Command Line Interface) or TUI (Terminal Line Interface)?
+ - What's your best GUI project?
# web-development
366673702533988363:
- How has Python helped you in web development?
+ - What tools do you use for web development?
+ - What is your favorite API library?
+ - What do you use for your frontend?
+ - What does your stack look like?
diff --git a/bot/resources/evergreen/starter.yaml b/bot/resources/evergreen/starter.yaml
index 53c89364..949220f9 100644
--- a/bot/resources/evergreen/starter.yaml
+++ b/bot/resources/evergreen/starter.yaml
@@ -20,3 +20,14 @@
- If you had $100 bill in your Easter Basket, what would you do with it?
- What would you do if you know you could succeed at anything you chose to do?
- If you could take only three things from your house, what would they be?
+- What's the best pastry?
+- What's your favourite kind of soup?
+- What is the most useless talent that you have?
+- Would you rather fight 100 duck sized horses or one horse sized duck?
+- What is your favourite color?
+- What's your favourite type of weather?
+- Tea or coffee? What about milk?
+- Do you speak a language other than English?
+- What is your favorite TV show?
+- What is your favorite media genre?
+- How many years have you spent coding?