async def catalog(ctx: Context): "Create a permanent roles catalog post in the current channel." roles = settings['roles.catalog'].get(ctx) if roles is None or len(roles) == 0: await ctx.send(':person_shrugging: There are no available ' 'self-service roles.') log.warn(f'{ctx.author} attempted to post roles catalog, but no roles ' 'are available') return guild_id = str(ctx.guild.id) if guild_id in directories: existing = directories[guild_id] chan: TextChannel = ctx.guild.get_channel(existing['channel']) if chan is not None: try: msg = await chan.fetch_message(existing['message']) await msg.delete() except NotFound: pass msg = await _get_message(ctx) directories[guild_id] = {'message': msg.id, 'channel': ctx.channel.id} log.info(f'{ctx.author} posted roles catalog to {ctx.channel}') await ctx.message.delete()
async def _update_poll(member: Member, message: Message, emoji: str, adjustment: int): poll = polls[message.id] opts = poll['options'] opt = opts[emoji] opt['count'] += adjustment acted = False if adjustment > 0: if member.id in opt['votes']: # correct count if they voted but reacts are out of sync opt['count'] -= adjustment else: acted = True opt['votes'].add(member.id) elif adjustment < 0 and member.id in opt['votes']: acted = True opt['votes'].remove(member.id) opts[emoji] = opt poll['options'] = opts polls[message.id] = poll verb = 'voted' if adjustment > 0 else 'retracted vote' if acted: log.info(f'{member} {verb} for {emoji} - {poll["prompt"]}') else: log.warn(f'Ignored vote for {emoji} by {member} in {message.id} - ' f'{poll["prompt"]} (reacts are out of sync)') await message.edit(embed=_get_embed(poll))
async def set(self, ctx, name: str, *, value: str, channel: Optional[str] = None): """ Change/view a setting's value If [name] is not provided, a list of all settings (but not their values) will be shown. If [value] is not provided, the setting's current value (and its default) will be shown, instead. Note that channel-dependent settings must be set and viewed in relevant channels. Use the 'channel' parameter to specify a channel other than the current one. Example: set key value channel=#lobby """ chan = ctx.channel matches = re.findall(r'\bchannel=(.+?)$', value) val = value if len(matches) > 0: chan = await chan_converter.convert(ctx, matches[0]) val = re.sub(r'\bchannel=.+?$', '', value).strip() if name not in settings: await ctx.send(MSG_NO_SETTING) log.warn(f'{ctx.author} attempted to set nonexistent setting: ' f'{name} in {chan}') return if settings[name].set(ctx, val, channel=chan): await ctx.send(f':thumbsup: Value updated.') log.info(f'{ctx.author} updated setting {name}: {val} in {chan}') else: await ctx.send(f':thumbsdown: Error updating value.') log.warn(f'{ctx.author} failed to update setting {name}: {val} ' f'in {chan}')
async def poll(ctx: Context, *, options: str): """ Create a poll To create a poll, options must be provided. Separate options with commas. You may provide a prompt if you wish by encasing the first argument to the command in brackets. To delete a poll, use both the Delete and Confirm reactions. Only a moderator, administrator, or the creator of the poll may delete it. Examples: !poll The dress is green, The dress is gold !poll [Do you see what I see?] Yes, No """ match = re.match(r'^(?:\[([^\]]+)\]\s*)?(.+)$', options) if match is None: await ctx.message.add_reaction(THUMBS_DOWN) log.warn(f'{ctx.author} Provided invalid arguments: {options}') return prompt, qstr = match.groups() count = 1 opts = {} for s in qstr.split(','): emoji = f'{count}{DIGIT_SUFFIX}' opt = s.strip() opts[emoji] = {'text': opt, 'count': 0, 'votes': set([])} count += 1 poll = { 'timestamp': datetime.utcnow(), 'author': ctx.author.display_name, 'author_id': ctx.author.id, 'avatar': str(ctx.author.avatar_url), 'prompt': prompt, 'options': opts, 'open': 1, 'delete': set([]), 'confirm': set([]) } msg: Message = await ctx.send(embed=_get_embed(poll)) for emoji in opts.keys(): await msg.add_reaction(emoji) await msg.add_reaction(PROHIBITED) await msg.add_reaction(WASTEBASKET) await msg.add_reaction(CHECK_MARK) polls[msg.id] = poll log.info(f'{ctx.author} created poll: {poll!r}') await ctx.message.delete()
async def clear(self, ctx, name): "Reset setting <name> to its default value" if name not in settings: log.warn(f'{ctx.author} attempted to clear nonexistent setting: ' f'{name}') await ctx.send(MSG_NO_SETTING) return settings[name].set(ctx, None, raw=True) await ctx.send(':negative_squared_cross_mark: Setting cleared.') log.info(f'{ctx.author} cleared setting {name}')
async def check_name_only(ctx: Context): "If the bot wasn't mentioned, refuse the command." # don't bother with DMs if isinstance(ctx.channel, DMChannel): return True if (settings['nameonly'].get(ctx) \ or settings['nameonly.channel'].get(ctx)) \ and not ctx.bot.user.mentioned_in(ctx.message): log.warn(f'{ctx.author} attempted command without mentioning bot') return False return True
async def desc(self, ctx, name): "View description of setting <name>" if name not in settings: await ctx.send(MSG_NO_SETTING) log.warn(f'{ctx.author} attempted to view description of ' f'nonexistent setting {name}') return setting = settings[name] if setting.description is None: await ctx.send(':person_shrugging: No description set.') else: await ctx.send(f':book: `{setting.name}` ' f'_(Channel: **{str(setting.channel)}**)_\n' f'> {setting.description}') log.info(f'{ctx.author} viewed description of setting {name}')
async def roles(ctx: Context): "Manage your membership in available roles" expiry_raw = settings['roles.postexpiry'].get(ctx) expiry = seconds_to_str(expiry_raw) roles = settings['roles.catalog'].get(ctx) if roles is None or len(roles) == 0: await ctx.send(':person_shrugging: There are no available ' 'self-service roles.') log.warn(f'{ctx.author} invoked roles self-service, but no roles are ' 'available') return roles = roles[:10] embed = Embed(title=f':billed_cap: Available roles', description='Use post reactions to manage role membership', color=Color.purple()) embed.set_footer(text=f'This post will be deleted in {expiry}.') count = 0 for role in sorted(roles, key=lambda x: x.lower()): embed.add_field(name=f'{count}{DIGIT_SUFFIX} {role}', value='\u200b') count += 1 msg: Message = await ctx.send(embed=embed) for i in range(0, count): await msg.add_reaction(f'{i}{DIGIT_SUFFIX}') posts[msg.id] = { 'guild': ctx.guild.id, 'channel': ctx.channel.id, 'expiry': datetime.utcnow() + timedelta(seconds=expiry_raw) } log.info(f'{ctx.author} invoked roles self-service') loop.call_later(expiry_raw, _delete, msg.id) await ctx.message.delete()
async def clear(self, ctx, name, *, params: channel_arg = channel_arg.defaults()): """ Reset setting <name> to its default value Use the 'channel' parameter to specify a channel other than the current one. Example: clear key channel=#lobby """ channel = ctx.channel if 'channel' not in params else params['channel'] if name not in settings: log.warn(f'{ctx.author} attempted to clear nonexistent setting: ' f'{name} in {channel}') await ctx.send(MSG_NO_SETTING) return settings[name].set(ctx, None, raw=True, channel=channel) await ctx.send(':negative_squared_cross_mark: Setting cleared.') log.info(f'{ctx.author} cleared setting {name} in {channel}')
async def roles(ctx: Context): "Manage your membership in available roles" expiry_raw = settings['roles.postexpiry'].get(ctx) expiry = seconds_to_str(expiry_raw) roles = settings['roles.catalog'].get(ctx) if roles is None or len(roles) == 0: await ctx.send(':person_shrugging: There are no available ' 'self-service roles.') log.warn(f'{ctx.author} invoked roles self-service, but no roles are ' 'available') return msg = await _get_message(ctx, expiry=expiry) posts[msg.id] = {'guild': ctx.guild.id, 'channel': ctx.channel.id, 'expiry': datetime.utcnow() + timedelta(seconds=expiry_raw)} log.info(f'{ctx.author} invoked roles self-service') loop.call_later(expiry_raw, _delete, msg.id) await ctx.message.delete()
async def on_ready(): "Clear expired/missing roles posts on startup." # clean up missing directories for guild_id, directory in directories.items(): try: guild: Guild = bot.get_guild(int(guild_id)) chan: TextChannel = guild.get_channel(directory['channel']) msg = await chan.fetch_message(directory['message']) except NotFound: log.warn(f'Deleted missing directory post for {guild_id}') del directories[guild_id] # clean up expired posts now = datetime.utcnow() for id, msg in posts.items(): if msg['expiry'] <= now: _delete(id) else: expiry: datetime = msg['expiry'] diff = (expiry - now).total_seconds() loop.call_later(diff, _delete, id) log.debug(f'Scheduled deletion of self-service post {id}')
async def set(self, ctx, name: typing.Optional[str] = None, *value): """ Change/view a setting's value If [name] is not provided, a list of all settings (but not their values) will be shown. If [value] is not provided, the setting's current value (and its default) will be shown, instead. Note that channel-dependent settings must be set and viewed in relevant channels. """ if name is None: settings_str = '**, **'.join(sorted(settings.keys())) await ctx.send(f':gear: All settings: **{settings_str}**') log.info(f'{ctx.author} viewed all settings') return if name not in settings: await ctx.send(MSG_NO_SETTING) log.warn(f'{ctx.author} attempted to set nonexistent setting: ' f'{name}') return val = ' '.join(value) if not len(val): val = settings[name].get(ctx) default = settings[name].default await ctx.send(f':gear: `{name}`\n' f'>>> Value: `{repr(val)}`\n' f'Default: `{repr(default)}`') log.info(f'{ctx.author} viewed setting {name}') elif settings[name].set(ctx, val): await ctx.send(f':thumbsup: Value updated.') log.info(f'{ctx.author} updated setting {name}: {val}') else: await ctx.send(f':thumbsdown: Error updating value.') log.warn(f'{ctx.author} failed to update setting {name}: {val}')