diff options
Diffstat (limited to 'bot')
| -rw-r--r-- | bot/bot.py | 17 | ||||
| -rw-r--r-- | bot/constants.py | 16 | ||||
| -rw-r--r-- | bot/exts/easter/earth_photos.py | 61 | ||||
| -rw-r--r-- | bot/exts/evergreen/connect_four.py | 450 | ||||
| -rw-r--r-- | bot/exts/evergreen/error_handler.py | 9 | ||||
| -rw-r--r-- | bot/exts/evergreen/issues.py | 6 | ||||
| -rw-r--r-- | bot/exts/evergreen/wikipedia.py | 164 | ||||
| -rw-r--r-- | bot/resources/evergreen/py_topics.yaml | 53 | ||||
| -rw-r--r-- | bot/resources/evergreen/starter.yaml | 11 | 
9 files changed, 675 insertions, 112 deletions
| @@ -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? | 
