Пример #1
0
    async def spellbot(self, prefix, params, message):
        """
        Configure SpellBot for your server. _Requires the "SpellBot Admin" role._

        The following subcommands are supported:
        * `config`: Just show the current configuration for this server.
        * `channel <list>`: Set SpellBot to only respond in the given list of channels.
        * `prefix <string>`: Set SpellBot prefix for commands in text channels.
        * `scope <server|channel>`: Set matchmaking scope to server-wide or channel-only.
        * `expire <number>`: Set the number of minutes before pending games expire.
        * `friendly <on|off>`: Allow or disallow friendly queueing with mentions.
        & <subcommand> [subcommand parameters]
        """
        if not is_admin(message.channel, message.author):
            return await message.channel.send(s("not_admin"))
        if not params:
            return await message.channel.send(s("spellbot_missing_subcommand"))
        self.ensure_server_exists(message.channel.guild.id)
        command = params[0]
        if command == "channels":
            await self.spellbot_channels(prefix, params[1:], message)
        elif command == "prefix":
            await self.spellbot_prefix(prefix, params[1:], message)
        elif command == "scope":
            await self.spellbot_scope(prefix, params[1:], message)
        elif command == "expire":
            await self.spellbot_expire(prefix, params[1:], message)
        elif command == "friendly":
            await self.spellbot_friendly(prefix, params[1:], message)
        elif command == "config":
            await self.spellbot_config(prefix, params[1:], message)
        else:
            await message.channel.send(
                s("spellbot_unknown_subcommand", command=command))
Пример #2
0
 async def spellbot_prefix(self, prefix, params, message):
     if not params:
         return await message.channel.send(s("spellbot_prefix_none"))
     prefix_str = params[0][0:10]
     server = (self.session.query(Server).filter(
         Server.guild_xid == message.channel.guild.id).one_or_none())
     server.prefix = prefix_str
     return await message.channel.send(
         s("spellbot_prefix", prefix=prefix_str))
Пример #3
0
    async def leave(self, prefix, params, message):
        """
        Leave your place in the queue.
        """
        user = self.ensure_user_exists(message.author)
        if not user.waiting:
            return await message.channel.send(s("leave_already"))

        user.dequeue()
        await message.channel.send(s("leave"))
Пример #4
0
 async def spellbot_scope(self, prefix, params, message):
     if not params:
         return await message.channel.send(s("spellbot_scope_none"))
     scope_str = params[0].lower()
     if scope_str not in ("server", "channel"):
         return await message.channel.send(s("spellbot_scope_bad"))
     server = (self.session.query(Server).filter(
         Server.guild_xid == message.channel.guild.id).one_or_none())
     server.scope = scope_str
     await message.channel.send(s("spellbot_scope", scope=scope_str))
Пример #5
0
 async def spellbot_expire(self, prefix, params, message):
     if not params:
         return await message.channel.send(s("spellbot_expire_none"))
     expire = to_int(params[0])
     if not expire or not (0 < expire <= 60):
         return await message.channel.send(s("spellbot_expire_bad"))
     server = (self.session.query(Server).filter(
         Server.guild_xid == message.channel.guild.id).one_or_none())
     server.expire = expire
     await message.channel.send(s("spellbot_expire", expire=expire))
Пример #6
0
 async def spellbot_channels(self, prefix, params, message):
     if not params:
         return await message.channel.send(s("spellbot_channels_none"))
     self.session.query(Channel).filter_by(
         guild_xid=message.channel.guild.id).delete()
     for param in params:
         self.session.add(
             Channel(guild_xid=message.channel.guild.id, name=param))
     await message.channel.send(
         s("spellbot_channels",
           channels=", ".join([f"#{param}" for param in params])))
Пример #7
0
 async def spellbot_friendly(self, prefix, params, message):
     if not params:
         return await message.channel.send(s("spellbot_friendly_none"))
     friendly_str = params[0].lower()
     if friendly_str not in ("off", "on"):
         return await message.channel.send(s("spellbot_friendly_bad"))
     server = (self.session.query(Server).filter(
         Server.guild_xid == message.channel.guild.id).one_or_none())
     server.friendly = friendly_str == "on"
     await message.channel.send(
         s("spellbot_friendly", friendly=friendly_str))
Пример #8
0
 async def status(self, prefix, params, message):
     """
     Show some details about the queues on your server.
     """
     server = self.ensure_server_exists(message.channel.guild.id)
     average = WaitTime.average(
         self.session,
         guild_xid=server.guild_xid,
         channel_xid=message.channel.id,
         scope=server.scope,
         window_min=AVG_QUEUE_TIME_WINDOW_MIN,
     )
     if average:
         wait = naturaldelta(timedelta(seconds=average))
         await message.channel.send(s("status", wait=wait))
     else:
         await message.channel.send(s("status_unknown"))
Пример #9
0
async def safe_react_error(message: discord.Message) -> None:
    try:
        await message.add_reaction(RED_X)
    except discord.errors.Forbidden:
        await message.channel.send(s("reaction_permissions_required"))
    except discord.errors.HTTPException as e:
        logger.exception(
            "warning: discord (%s): could react to message: %s",
            _user_or_guild_log_part(message),
            e,
        )
Пример #10
0
 async def process(self, message, prefix):
     """Process a command message."""
     tokens = message.content.split(" ")
     request, params = tokens[0].lstrip(prefix).lower(), tokens[1:]
     params = list(filter(None,
                          params))  # ignore any empty string parameters
     if not request:
         return
     matching = [
         command for command in self.commands if command.startswith(request)
     ]
     if not matching:
         await message.channel.send(s("not_a_command", request=request),
                                    file=None)
         return
     if len(matching) > 1 and request not in matching:
         possible = ", ".join(f"{prefix}{m}" for m in matching)
         await message.channel.send(s("did_you_mean", possible=possible),
                                    file=None)
     else:
         command = request if request in matching else matching[0]
         method = getattr(self, command)
         if not method.allow_dm and str(message.channel.type) == "private":
             return await message.author.send(s("no_dm"))
         logging.debug("%s%s (params=%s, message=%s)", prefix, command,
                       params, message)
         self.session = self.data.Session()
         try:
             await method(prefix, params, message)
             self.session.commit()
         except exc.SQLAlchemyError as e:
             logging.exception(f"error: {request}:", e)
             self.session.rollback()
             raise
         finally:
             self.session.close()
Пример #11
0
    async def begin(self, prefix, params, message):
        """
        Confirm creation of games for the given event id. _Requires the
        "SpellBot Admin" role._
        & <event id>
        """
        if not is_admin(message.channel, message.author):
            return await message.channel.send(s("not_admin"))

        if not params:
            return await message.channel.send(s("begin_no_params"))

        event_id = to_int(params[0])
        if not event_id:
            return await message.channel.send(s("begin_bad_event"))

        event = self.session.query(Event).filter(
            Event.id == event_id).one_or_none()
        if not event:
            return await message.channel.send(s("begin_bad_event"))

        if event.started:
            return await message.channel.send(s("begin_event_already_started"))

        for game in event.games:
            players_str = ", ".join(
                sorted([f"<@{user.xid}>" for user in game.users]))

            found_discord_users = []
            for game_user in game.users:
                discord_user = self.get_user(game_user.xid)
                if not discord_user:  # game_user has left the server since event created
                    warning = s("begin_user_left", players=players_str)
                    await message.channel.send(warning)
                else:
                    found_discord_users.append(discord_user)
            if len(found_discord_users) != len(game.users):
                continue

            game.url = self.create_game()
            game.status = "started"
            response = game.to_str()
            for discord_user in found_discord_users:
                await discord_user.send(response)

            await message.channel.send(
                s("game_created",
                  id=game.id,
                  url=game.url,
                  players=players_str))
Пример #12
0
async def safe_remove_reaction(message: discord.Message, emoji: str,
                               user: discord.User) -> None:
    try:
        await message.remove_reaction(emoji, user)
    except discord.errors.Forbidden:
        await message.channel.send(s("reaction_permissions_required"))
    except (
            discord.errors.HTTPException,
            discord.errors.NotFound,
            discord.errors.InvalidArgument,
    ) as e:
        logger.exception(
            "warning: discord (%s): could not remove reaction: %s",
            _user_or_guild_log_part(message),
            e,
        )
Пример #13
0
    async def help(self, prefix, params, message):
        """
        Sends you this help message.
        """
        usage = ""
        for command in self.commands:
            method = getattr(self, command)
            doc = method.__doc__.split("&")
            use, params = doc[0], ", ".join(
                [param.strip() for param in doc[1:]])
            use = inspect.cleandoc(use)

            transformed = ""
            for line in use.split("\n"):
                if line:
                    if line.startswith("*"):
                        transformed += f"\n{line}"
                    else:
                        transformed += f"{line} "
                else:
                    transformed += "\n\n"
            use = transformed
            use = use.replace("\n", "\n> ")
            use = re.sub(r"([^>])\s+$", r"\1", use, flags=re.M)

            title = f"{prefix}{command}"
            if params:
                title = f"{title} {params}"
            usage += f"\n`{title}`"
            usage += f"\n>  {use}"
            usage += "\n"
        usage += "---"
        usage += (" \nPlease report any bugs and suggestions at"
                  " <https://github.com/lexicalunit/spellbot/issues>!")
        usage += "\n"
        usage += (
            "\n💜 You can help keep SpellBot running by supporting me on Ko-fi! "
            "<https://ko-fi.com/Y8Y51VTHZ>")
        await message.channel.send(s("dm_sent"))
        for page in paginate(usage):
            await message.author.send(page)
Пример #14
0
 async def cleanup_expired_games(self):
     """Culls games older than the given window of minutes."""
     session = self.data.Session()
     try:
         expired = Game.expired(session)
         for game in expired:
             for user in game.users:
                 discord_user = self.get_user(user.xid)
                 if discord_user:
                     await discord_user.send(
                         s("expired", window=game.server.expire))
                 user.queued_at = None
                 user.game = None
             game.tags = []  # cascade delete tag associations
             session.delete(game)
         session.commit()
     except exc.SQLAlchemyError as e:
         logging.exception("error: cleanup_expired_games:", e)
         session.rollback()
         raise
     finally:
         session.close()
Пример #15
0
    async def game(self, prefix, params, message):
        """
        Create a game between mentioned users. _Requires the "SpellBot Admin" role._

        Operates similarly to the `!play` command with a few key deferences. First, see
        that command's usage help for more details. Then, here are the differences:
        * The user who issues this command is **NOT** added to the game themselves.
        * You must mention all of the players to be seated in the game.
        * Optional: Add a message by using " -- " followed by the message content.
        & [similar parameters as !play] [-- An optional additional message to send.]
        """
        if not is_admin(message.channel, message.author):
            return await message.channel.send(s("not_admin"))

        optional_message = None
        try:
            sentry = params.index("--")
            optional_message = " ".join(params[sentry + 1:])
            params = params[0:sentry]
        except ValueError:
            pass

        if optional_message and len(optional_message) >= 255:
            return await message.channel.send(s("game_message_too_long"))

        params = [param.lower() for param in params]
        mentions = message.mentions if message.channel.type != "private" else []

        power, size = power_and_size_from_params(params)
        if not size or not (1 < size <= 4):
            return await message.channel.send(s("game_size_bad"))
        if power and not (1 <= power <= 10):
            return await message.channel.send(s("game_power_bad"))

        if len(mentions) > size:
            return await message.channel.send(
                s("game_too_many_mentions", size=size))
        elif len(mentions) < size:
            return await message.channel.send(
                s("game_too_few_mentions", size=size))

        mentioned_users = []
        for mentioned in mentions:
            mentioned_user = self.ensure_user_exists(mentioned)
            if mentioned_user.waiting:
                mentioned_user.dequeue()
            mentioned_users.append(mentioned_user)
        self.session.commit()

        tag_names = tag_names_from_params(params)
        if len(tag_names) > 5:
            return await message.channel.send(s("game_too_many_tags"))

        tags = []
        for tag_name in tag_names:
            tag = self.session.query(Tag).filter_by(name=tag_name).first()
            if not tag:
                tag = Tag(name=tag_name)
                self.session.add(tag)
            tags.append(tag)

        server = self.ensure_server_exists(message.channel.guild.id)
        self.session.commit()

        now = datetime.utcnow()
        expires_at = now + timedelta(minutes=server.expire)
        url = self.create_game()
        game = Game(
            channel_xid=message.channel.id
            if server.scope == "channel" else None,
            created_at=now,
            expires_at=expires_at,
            guild_xid=server.guild_xid,
            power=power,
            size=size,
            updated_at=now,
            url=url,
            status="started",
            message=optional_message,
            users=mentioned_users,
            tags=tags,
        )
        self.session.add(game)
        self.session.commit()

        player_response = game.to_str()
        for player in mentioned_users:
            discord_user = self.get_user(player.xid)
            await discord_user.send(player_response)
            player.queued_at = None

        players_str = ", ".join(
            sorted([f"<@{user.xid}>" for user in mentioned_users]))
        await message.channel.send(
            s("game_created", id=game.id, url=game.url, players=players_str))
Пример #16
0
    async def play(self, prefix, params, message):
        """
        Enter a play queue for a game on SpellTable.

        You can get in a queue with a friend by mentioning them in the command with the @
        character. You can also change the number of players from the default of four by
        using, for example, `!play size:2` to create a two player game.

        Up to five tags can be given as well. For example, `!play no-combo proxy` has
        two tags: `no-combo` and `proxy`. Look on your server for what tags are being
        used by your community members. Tags can **not** be a number like `5`. Be careful
        when using tags because the matchmaker will only pair you up with other players
        who've used **EXACTLY** the same tags.

        You can also specify a power level like `!play power:7` for example and the
        matchmaker will attempt to find a game with similar power levels for you. Note
        that players who specify a power level will never get paired up with players who
        have not, and vice versa. You will also not be matched up _exactly_ by power level
        as there is a fudge factor involved.

        If your server's admins have set the scope for your server to "channel", then
        matchmaking will only happen between other players who run this command in the
        same channel as you did. The default scope for matchmaking is server-wide.
        & [@mention-1] [@mention-2] [...] [size:N] [power:N] [tag-1] [tag-2] [...]
        """
        user = self.ensure_user_exists(message.author)
        if user.waiting:
            return await message.channel.send(s("play_already"))

        params = [param.lower() for param in params]
        mentions = message.mentions if message.channel.type != "private" else []

        server = self.ensure_server_exists(message.channel.guild.id)
        self.session.commit()

        friendly = server.friendly if server.friendly is not None else True

        power, size = power_and_size_from_params(params)
        if not size or not (1 < size < 5):
            return await message.channel.send(s("play_size_bad"))
        if power and not (1 <= power <= 10):
            return await message.channel.send(s("play_power_bad"))

        if friendly and len(mentions) + 1 > size:
            return await message.channel.send(s("play_too_many_mentions"))

        mentioned_users = []
        if friendly:
            for mentioned in mentions:
                mentioned_user = self.ensure_user_exists(mentioned)
                if mentioned_user.waiting:
                    return await message.channel.send(
                        s("play_mention_already", user=mentioned))
                mentioned_users.append(mentioned_user)

        tag_names = tag_names_from_params(params)
        if len(tag_names) > 5:
            return await message.channel.send(s("play_too_many_tags"))

        tags = []
        for tag_name in tag_names:
            tag = self.session.query(Tag).filter_by(name=tag_name).first()
            if not tag:
                tag = Tag(name=tag_name)
                self.session.add(tag)
            tags.append(tag)

        user.enqueue(
            server=server,
            channel_xid=message.channel.id,
            include=mentioned_users,
            size=size,
            power=power,
            tags=tags,
        )
        self.session.commit()

        found_discord_users = []
        if len(user.game.users) == size:
            for game_user in user.game.users:
                discord_user = self.get_user(game_user.xid)
                if not discord_user:  # game_user has left the server since queueing
                    game_user.dequeue()
                else:
                    found_discord_users.append(discord_user)

        await message.channel.send(s("dm_sent"))
        if len(found_discord_users
               ) == size:  # all players matched, game is ready
            user.game.url = self.create_game()
            user.game.status = "started"
            game_created_at = datetime.utcnow()
            response = user.game.to_str()
            for game_user, discord_user in zip(user.game.users,
                                               found_discord_users):
                await discord_user.send(response)
                WaitTime.log(
                    self.session,
                    guild_xid=server.guild_xid,
                    channel_xid=message.channel.id,
                    seconds=(game_created_at -
                             game_user.queued_at).total_seconds(),
                )
                game_user.queued_at = None
        else:  # still waiting on more players, game is pending
            response = user.game.to_str()
            await message.author.send(response)
            if friendly:
                for mention in mentions:
                    await mention.send(response)
Пример #17
0
    async def event(self, prefix, params, message):
        """
        Create many games in batch from an attached CSV data file. _Requires the
        "SpellBot Admin" role._

        For example, if your event is for a Modern tournement you might attach a CSV file
        with a comment like `!event Player1Username Player2Username`. This would assume
        that the players' discord user names are found in the "Player1Username" and
        "Player2Username" CSV columns. The game size is deduced from the number of column
        names given, so we know the games created in this example are `size:2`.

        Games will not be created immediately. This is to allow you to verify things look
        ok. This command will also give you directions on how to actually start the games
        for this event as part of its reply.
        * Optional: Add a message by using " -- " followed by the message content.
        & <column 1> <column 2> ... <column 3> [-- An optional message to add.]
        """
        if not is_admin(message.channel, message.author):
            return await message.channel.send(s("not_admin"))

        if not message.attachments:
            return await message.channel.send(s("event_no_data"))

        if not params:
            return await message.channel.send(s("event_no_params"))

        optional_message = None
        try:
            sentry = params.index("--")
            optional_message = " ".join(params[sentry + 1:])
            params = params[0:sentry]
        except ValueError:
            pass

        if optional_message and len(optional_message) >= 255:
            return await message.channel.send(s("game_message_too_long"))

        size = len(params)
        if not (1 < size <= 4):
            return await message.channel.send(s("event_bad_play_count"))

        attachment = message.attachments[0]

        if not attachment.filename.lower().endswith(".csv"):
            return await message.channel.send(s("event_not_csv"))

        bdata = await message.attachments[0].read()
        sdata = bdata.decode("utf-8")

        has_header = csv.Sniffer().has_header(sdata)
        if not has_header:
            return await message.channel.send(s("event_no_header"))

        server = self.ensure_server_exists(message.channel.guild.id)
        reader = csv.reader(StringIO(sdata))
        header = [column.lower().strip() for column in next(reader)]
        params = [param.lower().strip() for param in params]

        if any(param not in header for param in params):
            return await message.channel.send(s("event_no_header"))

        columns = [header.index(param) for param in params]

        event = Event()
        self.session.add(event)
        self.session.commit()

        members = message.channel.guild.members
        member_lookup = {
            member.name.lower().strip(): member
            for member in members
        }
        for i, row in enumerate(reader):
            values = [row[column] for column in columns]
            players_s = ", ".join([f'"{value}"' for value in values])
            player_names = [value.lower().strip() for value in values]

            if any(not player_name for player_name in player_names):
                warning = s("event_missing_player",
                            row=i + 1,
                            players=players_s)
                await message.channel.send(warning)
                continue

            player_discord_users = []
            for player_name in player_names:
                player_discord_user = member_lookup.get(player_name)
                if player_discord_user:
                    player_discord_users.append(player_discord_user)
                else:
                    warning = s(
                        "event_missing_user",
                        row=i + 1,
                        name=player_name,
                        players=players_s,
                    )
                    await message.channel.send(warning)

            if len(player_discord_users) != size:
                continue

            player_users = [
                self.ensure_user_exists(player_discord_user)
                for player_discord_user in player_discord_users
            ]

            for player_user in player_users:
                if player_user.waiting:
                    player_user.dequeue()
            self.session.commit()

            now = datetime.utcnow()
            expires_at = now + timedelta(minutes=server.expire)
            game = Game(
                created_at=now,
                expires_at=expires_at,
                guild_xid=message.channel.guild.id,
                size=size,
                updated_at=now,
                status="ready",
                message=optional_message,
                users=player_users,
                event=event,
            )
            self.session.add(game)
            self.session.commit()

        if not event.games:
            self.session.delete(event)
            return await message.channel.send(s("event_empty"))

        await message.channel.send(
            s("event_created", prefix=prefix, event_id=event.id))