예제 #1
0
    async def reset_proposal_channel(self, ctx):
        """Reset the proposal channel."""
        game = get_game(ctx)

        def deleter():
            del game.proposal_channel
            game.save()

        await command_templates.undesignate_channel(
            ctx,
            "proposal channel",
            get_game(ctx).proposal_channel,
            deleter=deleter,
            remove_warning=
            "This could seriously mess up any existing proposals.")
예제 #2
0
    async def reset_transaction_channel(self, ctx):
        """Reset the transaction channel."""
        game = get_game(ctx)

        def deleter():
            del game.transaction_channel
            game.save()

        await command_templates.undesignate_channel(
            ctx,
            "transaction channel",
            get_game(ctx).transaction_channel,
            deleter=deleter,
            remove_warning=
            "This could seriously mess up any existing transactions.")
예제 #3
0
 async def list_all_users(self, ctx):
     """List all tracked users."""
     await invoke_command(
         ctx, 'active list',
         *map(ctx.guild.get_member,
              map(int,
                  get_game(ctx).player_last_seen.keys())))
예제 #4
0
 async def list_active_users(self, ctx, *users: discord.Member):
     """List only users that are active (or those specified)."""
     game = get_game(ctx)
     description = ''
     users = list(users)
     if not users:
         for user_id in game.player_last_seen:
             try:
                 user = ctx.guild.get_member(int(user_id))
                 if self.is_active(ctx, user):
                     users.append(user)
             except:
                 pass
     users = sorted(
         sorted(users, key=member_sort_key(ctx)),
         key=lambda member: self.last_seen_diff(ctx, member) or 1e9)
     for user in users:
         last_seen = self.last_seen_diff(ctx, user)
         if last_seen is None:
             last_seen_text = "never"
         elif last_seen < 2:
             last_seen_text = "very recently"
         else:
             last_seen_text = f"about {format_hour_interval(self.last_seen_diff(ctx, user))} ago"
         description += f"{user.mention} was last seen **{last_seen_text}**"
         if self.is_active(ctx, user):
             description += " _(active)_"
         else:
             description += " _(inactive)_"
         description += "\n"
     await ctx.send(embed=make_embed(color=colors.EMBED_INFO,
                                     title=f"Active users ({len(users)})",
                                     description=description))
예제 #5
0
    async def proposal_info(self, ctx, *proposal_nums: int):
        """View information about proposals, such as age, number of for/against
        votes, and author.

        If no argument is specified, all open proposals will be selected.
        """
        game = get_game(ctx)
        proposal_nums = dedupe(proposal_nums)
        description = ''
        if not proposal_nums:
            proposal_nums = (int(n) for n, p in game.proposals.items()
                             if p['status'] == 'voting')
        proposal_nums = sorted(proposal_nums)
        if not proposal_nums:
            raise commands.UserInputError(
                "There are no open proposals. Please specify at least one proposal number."
            )

        async def do_it(include_url):
            description = '\n'.join(
                map(lambda m: self.get_proposal_message(ctx, m, include_url),
                    proposal_nums))
            await ctx.send(embed=make_embed(color=colors.EMBED_INFO,
                                            title="Proposal information",
                                            description=description))

        try:
            await do_it(True)
        except discord.HTTPException:
            await do_it(False)
예제 #6
0
    async def active_cutoff(self, ctx, new_cutoff: int = None):
        """Set or view the the active user cutoff time period.

        `new_cutoff` must be specified as an integer number of hours.
        """
        game = get_game(ctx)
        description = f"The current active user cutoff is **{format_hour_interval(game.active_cutoff)}**."
        if new_cutoff is None:
            await ctx.send(embed=make_embed(color=colors.EMBED_INFO,
                                            title="Active user cutoff time",
                                            description=description))
            return
        description += f" Change it to **{format_hour_interval(new_cutoff)}**?"
        m = await ctx.send(
            embed=make_embed(color=colors.EMBED_ASK,
                             title="Change active user cutoff time?",
                             description=description))
        response = await react_yes_no(ctx, m)
        if response == 'y':
            game.active_cutoff = new_cutoff
            game.save()
        await m.edit(embed=make_embed(
            color=YES_NO_EMBED_COLORS[response],
            title=
            f"Active user cutoff time change {YES_NO_HUMAN_RESULT[response]}"))
예제 #7
0
 async def add_currency(self, ctx, currency_name: str, color: discord.Color,
                        *aliases: str):
     """Create a new currency."""
     game = get_game(ctx)
     currency_name = currency_name.lower()
     aliases = list(map(str.lower, aliases))
     for s in [currency_name] + aliases:
         if game.get_currency(s):
             raise commands.UserInputError(
                 f"Currency name '{s}' is already used.")
     description = f"Color: {format_discord_color(color)}"
     description += "\nAliases: " + (", ".join(f"`{a}`" for a in aliases)
                                     or "(none)")
     m = await ctx.send(
         embed=make_embed(color=colors.EMBED_ASK,
                          title=f"Create currency '{currency_name}'?",
                          description=description))
     response = await react_yes_no(ctx, m)
     await m.edit(
         embed=make_embed(color=YES_NO_EMBED_COLORS[response],
                          title=f"Currency '{currency_name}' established"
                          if response else "Currency creation " +
                          YES_NO_HUMAN_RESULT[response],
                          description=description))
     if response != 'y':
         return
     game.add_currency(currency_name, color=color, aliases=aliases)
예제 #8
0
    async def refresh_proposal(self, ctx, *proposal_nums: int):
        """Refresh one or more proposal messages.

        This is mostly useful for fixing minor glitches, or if voting rules have
        changed.
        """
        if not proposal_nums:
            await invoke_command_help(ctx)
            return
        game = get_game(ctx)
        proposal_nums = dedupe(proposal_nums)
        succeeded, failed = await game.refresh_proposal(*proposal_nums)
        description = ''
        if succeeded:
            if len(succeeded) == 1:
                description += f"Proposal {succeeded[0]} succeessfully refreshed.\n"
            else:
                description += f"{len(succeeded)}/{len(proposal_nums)} proposal messages succeessfully refreshed.\n"
        if failed:
            description += f"Proposal{'' if len(failed) == 1 else 's'} {human_list(map(str, failed))} could not be refreshed.\n"
        m = await ctx.send(embed=make_embed(
            color=colors.EMBED_ERROR if failed else colors.EMBED_SUCCESS,
            title="Refreshed proposal messages",
            description=description))
        await game.wait_delete_if_illegal(ctx.message, m)
예제 #9
0
 async def set_currency_color(self, ctx, currency_name: str,
                              color: discord.Color):
     game = get_game(ctx)
     currency = game.get_currency(currency_name)
     if currency is None:
         raise commands.UserInputError(
             f"No such currency '{currency_name}'.")
     currency['color'] = format_discord_color(color)
     game.save()
     await ctx.message.add_reaction(emoji.SUCCESS)
예제 #10
0
 async def set_currency_aliases(self, ctx, currency_name: str,
                                *new_aliases: str):
     game = get_game(ctx)
     currency = game.get_currency(currency_name)
     if currency is None:
         raise commands.UserInputError(
             f"No such currency '{currency_name}'.")
     currency['aliases'] = [s.lower() for s in new_aliases]
     game.save()
     await ctx.message.add_reaction(emoji.SUCCESS)
예제 #11
0
    async def proposal_remove(self, ctx, proposal_nums: commands.Greedy[int],
                              *, reason):
        """Remove one or more proposals (and renumber subsequent ones accordingly).

        You must be an admin or the owner of a proposal to remove it.

        proposal_nums -- A list of proposal numbers to remove
        reason -- Justification for removal (applies to all proposals removed)
        """
        if not proposal_nums:
            await invoke_command_help(ctx)
            return
        game = get_game(ctx)
        for n in proposal_nums:
            # Make sure proposal exists and make sure that the user has
            # permission to remove it.
            if not ((game.get_proposal(n)['author'] == ctx.author.id
                     and game.get_proposal(n)['timestamp'] <
                     datetime.utcnow().timestamp() + 60 * 60 * 6)
                    or is_bot_admin(ctx.author)):
                raise UserInputError(
                    f"You don't have permission to remove proposal #{n}.")
        proposal_amount = 'ALL' if proposal_nums == 'all' else len(
            proposal_nums)
        proposal_pluralized = f"proposal{'s' * (len(proposal_nums) != 1)}"
        m = await ctx.send(embed=make_embed(
            color=colors.EMBED_ASK,
            title=f"Remove {proposal_amount} {proposal_pluralized}?",
            description="Are you sure? This cannot be undone."))
        response = await react_yes_no(ctx, m)
        await m.edit(embed=make_embed(
            color=YES_NO_EMBED_COLORS[response],
            title=f"Proposal removal {YES_NO_HUMAN_RESULT[response]}"))
        if response == 'y':
            game = get_game(ctx)
            await game.remove_proposal(ctx.author,
                                       *proposal_nums,
                                       reason=reason,
                                       m=m)
            await game.wait_delete_if_illegal(ctx.message, m)
예제 #12
0
    async def repost_proposal(self, ctx, *proposal_nums: int):
        """Repost one or more proposal messages (and all subsequent ones).

        This command may repost potentially hundreds of messages, depending on
        how many proposals there are. USE IT WISELY.
        """
        if not proposal_nums:
            await invoke_command_help(ctx)
            return
        game = get_game(ctx)
        proposal_nums = dedupe(proposal_nums)
        await game.repost_proposal(*proposal_nums)
        await game.wait_delete_if_illegal(ctx.message)
예제 #13
0
    async def set_transaction_channel(self,
                                      ctx,
                                      channel: commands.
                                      TextChannelConverter = None):
        """Set the transaction channel.

        If no argument is supplied, then the current channel will be used.
        """
        game = get_game(ctx)

        def setter(new_channel):
            game.transaction_channel = new_channel
            game.save()

        await command_templates.designate_channel(
            ctx,
            "transaction channel",
            get_game(ctx).transaction_channel,
            new_channel=channel or ctx.channel,
            setter=setter,
            change_warning=
            "This could seriously mess up any existing transactions.")
예제 #14
0
 async def _submit_proposal(self, ctx, content):
     game = get_game(ctx)
     m = await ctx.send(embed=make_embed(color=colors.EMBED_ASK,
                                         title="Submit new proposal?",
                                         description=content))
     response = await react_yes_no(ctx, m)
     if ctx.channel.id == game.proposal_channel.id:
         await m.delete()
     else:
         await m.edit(embed=make_embed(
             color=YES_NO_EMBED_COLORS[response],
             title=f"Proposal submission {YES_NO_HUMAN_RESULT[response]}"))
     if response != 'y':
         return
     await game.submit_proposal(ctx, content.strip())
예제 #15
0
    async def clean_proposal_channel(self, ctx, limit: int = 100):
        """Clean unwanted messages from the proposal channel.

        limit -- Number of messages to search (0 = all)
        """
        game = get_game(ctx)
        if not game.proposal_channel:
            return
        message_iter = game.proposal_channel.history(limit=limit or None)
        proposal_message_ids = set(
            p.get('message') for p in game.proposals.values())
        unwanted_messages = message_iter.filter(
            lambda m: m.id not in proposal_message_ids)
        await game.proposal_channel.delete_messages(
            await unwanted_messages.flatten())
예제 #16
0
    async def clean_transaction_channel(self, ctx, limit: int = 100):
        """Clean unwanted messages from the transaction channel.

        limit -- Number of messages to search (0 = all)
        """
        game = get_game(ctx)
        if not game.transaction_channel:
            return
        message_iter = game.transaction_channel.history(limit=limit or None)
        # transaction_message_ids = set(t.get('message') for t in game.transactions)
        transaction_message_ids = set(game.transaction_messages)
        unwanted_messages = message_iter.filter(
            lambda m: m.id not in transaction_message_ids)
        await game.transaction_channel.delete_messages(
            await unwanted_messages.flatten())
예제 #17
0
 async def on_message(self, message):
     try:
         if message.author.bot:
             return  # Ignore bots.
         prefix = await self.bot.get_prefix(message)
         if type(prefix) is list:
             prefix = tuple(prefix)
         if message.content.startswith(prefix):
             return  # Ignore commands.
         ctx = await self.bot.get_context(message)
         if message.channel.id == get_game(ctx).proposal_channel.id:
             content = message.content.strip()
             await message.delete()
             await self._submit_proposal(ctx, content)
     except:
         pass
예제 #18
0
 def get_proposal_message(self, ctx, proposal_num, include_url=True):
     game = get_game(ctx)
     proposal = game.get_proposal(proposal_num)
     url = f'https://discordapp.com/channels/{ctx.guild.id}/{game.proposal_channel.id}/{proposal.get("message", "")}'
     age = format_time_interval(int(proposal['timestamp']),
                                datetime.utcnow().timestamp(),
                                include_seconds=False)
     for_votes = sum(proposal['votes']['for'].values())
     against_votes = sum(proposal['votes']['against'].values())
     abstain_votes = sum(proposal['votes']['abstain'].values())
     if include_url:
         s = f"**[#{proposal_num}]({url})**"
     else:
         s = f"**#{proposal_num}**"
     s += f" - **{age}** old - **{for_votes}** for; **{against_votes}** against"
     if abstain_votes:
         s += f"; **{abstain_votes}** abstain"
     return s
예제 #19
0
 async def _set_proposal_statuses(self,
                                  ctx,
                                  new_status,
                                  proposal_nums,
                                  reason=''):
     if not proposal_nums:
         await invoke_command_help(ctx)
         return
     game = get_game(ctx)
     succeeded, failed = await game.set_proposal_status(ctx.author,
                                                        new_status,
                                                        *proposal_nums,
                                                        reason=reason)
     if succeeded:
         await ctx.message.add_reaction(emoji.SUCCESS)
     if failed:
         await ctx.message.add_reaction(emoji.FAILURE)
     await game.wait_delete_if_illegal(ctx.message)
예제 #20
0
 async def remove_currency(self, ctx, currency_name: str):
     game = get_game(ctx)
     currency = game.get_currency(currency_name)
     if currency is None:
         raise commands.UserInputError(
             f"No such currency '{currency_name}'.")
     currency_name = currency.name
     m = await ctx.send(embed=make_embed(
         color=colors.EMBED_ASK,
         title=f"Delete currency '{currency_name}'?",
         description="Are you sure? This cannot be undone."))
     response = await react_yes_no(ctx, m)
     await m.edit(embed=make_embed(
         color=YES_NO_EMBED_COLORS[response],
         title=
         f"Currency deletion '{currency_name}' {YES_NO_HUMAN_RESULT[response]}"
     ))
     if response == 'y':
         del game.currencies[currency_name]
         game.save()
예제 #21
0
 async def list_currencies(self,
                           ctx,
                           user_or_currency: Union[discord.Member,
                                                   str] = None):
     """List player values for a given currency, or currency values for a given player."""
     game = get_game(ctx)
     currencies = game.currencies
     if isinstance(user_or_currency,
                   discord.Member) or user_or_currency is None:
         user = user_or_currency
         if user is None:
             title = "Currency list"
         else:
             title = f"Currency list for {user.display_name}#{user.discriminator}"
         if currencies:
             description = ''
             for c in currencies.values():
                 description += f"\N{BULLET} **{c['name'].capitalize()}** "
                 if user is None:
                     description += f"(`{c['color']}`"
                     if c['aliases']:
                         description += "; " + ", ".join(c['aliases'])
                     description += ")\n"
                 else:
                     description += f"\N{EN DASH} {c['players'].get(str(user.id), 0)}\n"
         else:
             description = "There are no defined currencies."
     else:
         c = game.get_currency(user_or_currency)
         if c is None:
             raise commands.UserInputError(
                 f"No user or currency: {user_or_currency}")
         title = f"Player list for {c['name']}"
         description = ''
         for user_id in sorted(c['players'].keys(),
                               key=member_sort_key(ctx.guild)):
             description += f"{ctx.guild.get_member(int(user_id)).mention} has **{c['players'].get(user_id)}**\n"
         if not description:
             description = "(none)"
     await ctx.send(embed=make_embed(
         color=colors.EMBED_INFO, title=title, description=description))
예제 #22
0
 async def on_raw_reaction_add(self, payload):
     ctx = await self.bot.get_context(await self.bot.get_channel(
         payload.channel_id).get_message(payload.message_id))
     game = get_game(ctx)
     if game.proposal_channel and payload.channel_id != game.proposal_channel.id:
         return
     if ctx.bot.get_user(payload.user_id).bot:
         return
     for proposal in game.proposals.values():
         if proposal.get('message') == payload.message_id:
             try:
                 vote_type = {
                     emoji.VOTE_FOR: 'for',
                     emoji.VOTE_AGAINST: 'against',
                     emoji.VOTE_ABSTAIN: 'abstain',
                 }[payload.emoji.name]
                 await game.vote(
                     proposal_num=proposal['n'],
                     vote_type=vote_type,
                     user_id=payload.user_id,
                 )
             except:
                 await ctx.message.remove_reaction(
                     payload.emoji, ctx.guild.get_member(payload.user_id))
예제 #23
0
 async def on_message(self, message):
     try:
         if message.author.bot:
             return  # Ignore bots.
         prefix = await self.bot.get_prefix(message)
         if type(prefix) is list:
             prefix = tuple(prefix)
         if message.content.startswith(prefix):
             return  # Ignore commands.
         ctx = await self.bot.get_context(message)
         game = get_game(ctx)
         if message.channel.id == game.transaction_channel.id:
             content = message.content.strip()
             await message.delete()
             words = []
             for word in content.split():
                 try:
                     words.append(await commands.MemberConverter().convert(
                         ctx, word))
                 except:
                     try:
                         words.append(float(word))
                     except:
                         words.append(word)
             try:
                 await invoke_command(ctx, 'transact', *words)
             except:
                 m = await ctx.send(embed=make_embed(
                     color=colors.EMBED_ERROR,
                     title="Error parsing transaction",
                     description=
                     f"Check spelling/capitalization or try an explicit `!transact` command.\n```\n{content}\n```"
                 ))
                 await game.wait_delete_if_illegal(m)
     except:
         pass
예제 #24
0
    async def vote(self,
                   ctx,
                   user: Optional[discord.User],
                   amount: Optional[MultiplierConverter],
                   vote_type: str,
                   proposal_nums: commands.Greedy[int],
                   *,
                   reason=''):
        """Vote on a proposal.

        user (optional)   -- The user whose votes to modify (defaults to command invoker)
        amount (optional) -- Amount of times to vote (defaults to `1x`)
        vote_type         -- See below
        proposal_nums     -- IDs of proposals on which to vote
        reason (optional) -- Justification for vote (applies to all votes)

        Valid vote types:
        - `for` (aliases: `+`)
        - `against` (aliases: `-`)
        - `abstain`
        - `remove` (aliases: `del`, `delete`, `rm`)

        Example usages:
        ```
        !vote for 14 16
        !vote 2x against 11
        !vote @SomeUser remove 12
        !vote 3x @SomeUser for 13 15 20
        ```
        Alternatively, you can simply react to a proposal message in the
        proposal channel.
        """
        # User input errors will be handled and displayed to the user elsewhere.
        vote_type = vote_type.lower()
        if amount is None:
            amount = 1
        user = user or ctx.author
        if user.id != ctx.author.id and not await is_bot_admin(ctx):
            raise commands.MissingPermissions(
                ["You aren't allowed to change others' votes."])
        if vote_type in ('for', '+'):
            vote_type = 'for'
        elif vote_type in ('against', '-'):
            vote_type = 'against'
        elif vote_type in ('abstain'):
            vote_type = 'abstain'
        elif vote_type in ('remove', 'del', 'delete', 'rm'):
            vote_type = 'remove'
        else:
            raise commands.UserInputError("Invalid vote type.")
        game = get_game(ctx)
        for proposal_num in proposal_nums:
            await game.vote(
                proposal_num=proposal_num,
                vote_type=vote_type,
                user_id=user.id,
                user_agent_id=ctx.author.id,
                count=amount,
                reason=reason,
            )
        await ctx.message.add_reaction(emoji.SUCCESS)
        await game.wait_delete_if_illegal(ctx.message)
예제 #25
0
 def last_seen_diff(self, ctx, user):
     this_hour = get_hourly_timestamp()
     last_seen_hour = get_game(ctx).player_last_seen.get(str(user.id))
     if last_seen_hour is None:
         return None
     return this_hour - last_seen_hour
예제 #26
0
 def is_active(self, ctx, user):
     last_seen_diff = self.last_seen_diff(ctx, user)
     if last_seen_diff is None:
         return None
     return last_seen_diff <= get_game(ctx).active_cutoff
예제 #27
0
 async def transaction_channel(self, ctx):
     """Manage the transaction channel."""
     if ctx.invoked_subcommand is ctx.command:
         await command_templates.display_designated_channel(
             ctx, "transaction channel",
             get_game(ctx).transaction_channel)
예제 #28
0
    async def transact(self, ctx, *transaction: Union[discord.Member, float,
                                                      str]):
        """Make a transaction.

        Transactions are of the following form:
        ```
        give/take/transfer {<number> <currency> [from {user}] [to {user}]} [for/because <reason ...>]
        ```... where `<...>` is required, `[...]` is optionial, and `{...}` may be repeated.

        Multiple transactions can be specified in one message. Only one reason
        can be specified, and the reason must be the last thing in the message.
        Basically, just type normal English and it'll probably work.

        You can also use the special word `me` to refer to yourself. `to` or
        `from` is still required though.

        The number `all` may be used for all of a user's currency. Otherwise all
        numbers must be integers/decimals (no fractions).

        Instead of using this command, it's usually easier to just type directly
        into the transaction channel.

        Examples:
        ```
        give 10 point to @SomeUser
        take 3 points from @SomeUser
        transfer 13.6 mooncheese from @SomeUser to @SomeOtherUser
        take 1 mooncheese from @SomeUser and give it to @SomeOtherUser because @SomeOtherUser deserves it more
        ```
        """
        game = get_game(ctx)
        multiplier = 0
        amount = 0
        currency_name = None
        transactions = []
        reason = []
        for word in transaction:
            if reason:
                reason.append(word)
                continue
            if isinstance(word, int):
                amount = word
                currency_name = None
            elif isinstance(word, float):
                amount = int(word) if word.is_integer() else word
                currency_name = None
            elif isinstance(word, str):
                word = word.lower()
                if word == 'to':
                    multiplier = +1
                elif word == 'from':
                    multiplier = -1
                elif word == 'me':
                    word = ctx.author
                elif word in ('for', 'because'):
                    reason.append(word)
                elif currency_name is None and game.get_currency(word):
                    currency_name = game.get_currency(word)['name']
            if isinstance(word, discord.Member):
                if not multiplier and amount and currency_name:
                    raise commands.UserInputError(
                        f"Not sure what to do with {word.mention}. (Specify amount and currency.)"
                    )
                # "+10 points to"    ->  +
                # "-10 points to"    ->  -
                # "+10 points from"  ->  -
                # "-10 points from"  ->  -
                if multiplier < 0 and amount < 0:
                    multiplier = +1
                if currency_name is None:
                    raise commands.UserInputError(
                        "No valid currency specified. Use `!currency list` to view them all."
                    )
                if not isfinite(amount):
                    raise commands.UserInputError(
                        f"`{amount}` is not a valid amount. Nice try.")
                transactions.append({
                    'amount': amount * multiplier,
                    'currency_name': currency_name,
                    'user_agent_id': ctx.author.id,
                    'user_id': word.id
                })
        if not transactions:
            raise commands.UserInputError("No users specified.")
        if len(transactions) == 1:
            human_count = "transaction"
        else:
            human_count = f"{len(transactions)} transactions"
        description = '\n'.join(map(game.format_transaction, transactions))
        m = await ctx.send(embed=make_embed(color=colors.EMBED_ASK,
                                            title=f"Authorize {human_count}?",
                                            description=description))
        response = await react_yes_no(ctx, m)
        reason = ' '.join(map(str, reason))
        if reason:
            for t in transactions:
                t['reason'] = reason
        await m.edit(embed=make_embed(
            color=YES_NO_EMBED_COLORS[response],
            title=f"{human_count.capitalize()} {YES_NO_HUMAN_RESULT[response]}",
            description=description))
        if response == 'y':
            for transaction in transactions:
                await game.transact(transaction)
        await game.wait_delete_if_illegal(m)
예제 #29
0
 async def proposal_channel(self, ctx):
     """Manage the proposal channel."""
     if ctx.invoked_subcommand is ctx.command:
         await command_templates.display_designated_channel(
             ctx, "proposal channel",
             get_game(ctx).proposal_channel)
예제 #30
0
 def update_last_seen(self, ctx, user):
     if self.last_seen_diff(ctx, user) != 0:
         game = get_game(ctx)
         game.player_last_seen[str(user.id)] = get_hourly_timestamp()
         game.save()