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 reset(self, ctx, *, params: channel_arg = channel_arg.defaults()): """ Reset Only whitelist Using this command will disable whitelisting behavior and remove the existing whitelist. Use the 'channel' parameter to specify a channel other than the current one. Example: only.reset channel=#lobby """ channel = params['channel'] if 'channel' in params else ctx.channel chan_id = str(channel.id) guild = str(ctx.guild.id) ours = onlies[guild] if guild in onlies else None ourchan = ours[chan_id] \ if ours is not None and chan_id in ours else None if ourchan is None: await ctx.send(':person_shrugging: None set.') return del ours[chan_id] if len(ours) == 0: del onlies[guild] else: onlies[guild] = ours await ctx.send(':boom: Reset.') log.info(f'{ctx.author} reset Only whitelist for {channel}')
async def on_raw_reaction_remove(payload: RawReactionActionEvent): "Handle on_reaction_remove event." if payload.user_id == bot.user.id: return directory = directories[payload.guild_id] \ if payload.guild_id in directories else None if payload.message_id not in posts and ( directory is None or payload.message_id != directory['message']): return split = str(payload.emoji).split('\ufe0f') if len(split) != 2: return guild: Guild = bot.get_guild(payload.guild_id) member: Member = guild.get_member(payload.user_id) fake_ctx = FakeContext(guild=guild) setting = settings['roles.catalog'].get(fake_ctx, raw=True) roles = sorted([r for r in guild.roles if r.id in setting], key=lambda x: x.name.lower()) which = int(split[0]) if which < 0 or which > len(roles): return role = roles[which] await member.remove_roles(role) log.info(f'{member} removed role {role}')
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 list(self, ctx, server: typing.Optional[bool] = False, *, params: channel_arg = channel_arg.defaults()): """ List all current channel's whitelisted commands Use the 'channel' parameter to specify a channel other than the current one. Example: only.list channel=#lobby" """ channel = params['channel'] if 'channel' in params else ctx.channel chan_id = str(channel.id) guild = str(ctx.guild.id) if guild not in onlies: onlies[guild] = dict() ours = onlies[guild] if channel not in ours: ours[channel] = set([]) output = '**, **'.join(ours[chan_id]) if not len(output): output = 'None' log.info(f'{ctx.author} viewed command whitelist for {channel}') await ctx.send(f':guard: **{output}**')
async def add(self, ctx, command, server: typing.Optional[bool] = False): """ Disable the given command Disables <command> in this channel. If [server] is True, then the command is disabled on the entire server. """ server_key = command.lower().strip() key = f'{server_key}#{ctx.channel.id}' guild = str(ctx.guild.id) if not ctx.guild.id in lobotomies: lobotomies[guild] = set([]) lobs = lobotomies[guild] if (key in lobs and not server) \ or (server_key in lobs and server): await ctx.send(f':newspaper: Already done.') return # if it's already in the opposite category (channel vs. server), # then clear it out if key in lobs and server: lobs.remove(key) elif server_key in lobs and not server: lobs.remove(server_key) lobs.add(server_key if server else key) lobotomies[guild] = lobs log.info(f'{ctx.author} lobotomized {server_key if server else key}') await ctx.send(f':brain: Done.')
async def remove(self, ctx, command, server: typing.Optional[bool] = False): """ Enable the given command Enables <command> in this channel. If [server] is True, then the command is enabled on the entire server. """ server_key = command.lower().strip() key = f'{server_key}#{ctx.channel.id}' guild = str(ctx.guild.id) lobs = lobotomies[guild] if guild in lobotomies else None if lobs is None: await ctx.send(':person_shrugging: None set.') return if (key in lobs and server) or (server_key in lobs and not server): await ctx.send(':thumbsdown: The opposite scope is ' 'currently set.') return lobs.remove(server_key if server else key) lobotomies[guild] = lobs log.info(f'{ctx.author} removed {server_key if server else key}') await ctx.send(':wastebasket: Removed.')
async def reset(self, ctx): """ Reset Only whitelist Using this command will disable whitelisting behavior and remove the existing whitelist. """ guild = str(ctx.guild.id) channel = str(ctx.channel.id) ours = onlies[guild] if guild in onlies else None ourchan = ours[channel] \ if ours is not None and channel in ours else None if ourchan is None: await ctx.send(':person_shrugging: None set.') return del ours[channel] if len(ours) == 0: del onlies[guild] else: onlies[guild] = ours await ctx.send(':boom: Reset.') log.info(f'{ctx.author} reset Only whitelist for {ctx.channel}')
async def remove(self, ctx, command): """ Remove the given command from the Only whitelist If whitelisting is enabled for this channel, the removed command can no longer be executed. """ guild = str(ctx.guild.id) channel = str(ctx.channel.id) ours = onlies[guild] if guild in onlies else None ourchan = ours[channel] \ if ours is not None and channel in ours else None if ourchan is None: await ctx.send(':person_shrugging: None set.') return ourchan.remove(command) if len(ourchan) == 0: del ours[channel] else: ours[channel] = ourchan if len(ours) == 0: del onlies[guild] else: onlies[guild] = ours log.info(f'{ctx.author} removed {command} from {ctx.channel} ' 'whitelist') await ctx.send(':wastebasket: Removed.')
async def add(self, ctx, command): """ Add the given command to the Only whitelist Enables <command> in this channel. """ guild = str(ctx.guild.id) channel = str(ctx.channel.id) if guild not in onlies: onlies[guild] = dict() ours = onlies[guild] if channel not in ours: ours[channel] = set([]) ourchan = ours[channel] if command in ourchan: await ctx.send(f':newspaper: Already done.') return ourchan.add(command) ours[channel] = ourchan onlies[guild] = ours log.info(f'{ctx.author} added {command} to {ctx.channel} whitelist') await ctx.send(f':shield: Done.')
async def on_raw_reaction_add(payload: RawReactionActionEvent): "Handle on_reaction_add event." if payload.user_id == bot.user.id or payload.message_id not in posts: return guild: Guild = bot.get_guild(payload.guild_id) channel: TextChannel = guild.get_channel(payload.channel_id) message: Message = await channel.fetch_message(payload.message_id) member: Member = guild.get_member(payload.user_id) split = str(payload.emoji).split('\ufe0f') if len(split) != 2: await message.remove_reaction(payload.emoji, member) return fake_ctx = FakeContext(guild=guild) setting = settings['roles.catalog'].get(fake_ctx, raw=True) roles = sorted([r for r in guild.roles if r.id in setting], key=lambda x: x.name.lower()) which = int(split[0]) if which < 0 or which > len(roles): await message.remove_reaction(payload.emoji, member) return role = roles[which] await member.add_roles(role) log.info(f'{member} added role {role}')
async def add(self, ctx, alias, command): "Add an alias of <alias> for <command>" guild = str(ctx.guild.id) if ctx.guild.id not in aliases: aliases[guild] = dict() als = aliases[guild] if alias in als: await ctx.send(f':newspaper: Already exists.') return cmd = ctx.bot.get_command(command) if cmd is None: await ctx.send(f':scream: No such command!') return als[alias] = command aliases[guild] = als log.info(f'{ctx.author} added alias {alias} for {command}') await ctx.send(f':sunglasses: Done.')
async def _delete(): prompt = poll['prompt'] delete = payload.member.id in poll['delete'] confirm = payload.member.id in poll['confirm'] if delete and confirm: await msg.delete() del polls[msg.id] log.info(f'{payload.member} deleted poll {msg.id} - {prompt}')
async def github(ctx): """ This bot is running on Aethersprite, an open source bot software built with discord.py. My source code is available for free. Contributions in the form of code, bug reports, and feature requests are all welcome. https://github.com/haliphax/aethersprite """ await ctx.send('For source code, feature requests, and bug reports, visit ' 'https://github.com/haliphax/aethersprite') log.info(f'{ctx.author} requested GitHub URL')
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 on_member_join(member: Member): "Greet members when they join." chan_setting = settings['greet.channel'].get(member) msg_setting = settings['greet.message'].get(member) if chan_setting is None or msg_setting is None: return channel = [c for c in member.guild.channels if c.name == chan_setting][0] log.info(f'Greeting new member {member} in {member.guild.name} ' f'#{channel.name}') await channel.send(msg_setting.format(name=member.display_name, nl='\n'))
async def list(self, ctx): "List all command aliases" guild = str(ctx.guild.id) if guild not in aliases: aliases[guild] = dict() als = aliases[guild] output = ', '.join([f'`{k}` => `{als[k]}`' for k in als.keys()]) if len(output) == 0: output = 'None' log.info(f'{ctx.author} viewed alias list') await ctx.send(f':detective: **{output}**')
async def gmt(ctx, *, offset: typing.Optional[str]): """ Get current time in GMT or offset by days, hours, and minutes. To get the current time, no arguments are necessary. To get offset time (e.g. 5 hours from now), provide values for days, hours, or minutes. For offsets in the past, use negative numbers. Example: !gmt 3d 6h 17m <-- would request an offset of 3 days, 6 hours, 17 minutes Arguments aren't validated, so anything goes... but please be reasonable. The command will silently fail if you choose an offset the bot can't process. """ delta = get_timespan_chunks(offset) if offset else (0, 0, 0) days, hours, minutes = delta thetime = (datetime.now(timezone.utc) + timedelta(days=days, hours=hours, minutes=minutes)) offset_str = thetime.strftime(DATETIME_FORMAT) await ctx.send(f':clock: {offset_str}') log.info(f'{ctx.author} requested GMT offset of {delta}: {offset_str}')
def _delete(id: int): if id not in posts: return post = posts[id] guild: Guild = bot.get_guild(post['guild']) channel: TextChannel = guild.get_channel(post['channel']) async def f(): try: msg: Message = await channel.fetch_message(id) await msg.delete() except NotFound: pass del posts[id] aio.ensure_future(f()) log.info(f'Deleted roles self-service post {id}')
async def remove(self, ctx, alias): "Remove <alias>" guild = str(ctx.guild.id) als = aliases[guild] if guild in aliases else None if als is None or alias not in als: await ctx.send(':person_shrugging: None set.') return del als[alias] if len(als) == 0: del aliases[guild] else: aliases[guild] = als log.info(f'{ctx.author} removed alias {alias}') await ctx.send(':wastebasket: Removed.')
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 remove(self, ctx, command, *, params: channel_arg = channel_arg.defaults()): """ Remove the given command from the Only whitelist If whitelisting is enabled for this channel, the removed command can no longer be executed. Use the 'channel' parameter to specify a channel other than the current one. Example: only.remove test channel=#lobby """ channel = params['channel'] if 'channel' in params else ctx.channel chan_id = str(channel.id) guild = str(ctx.guild.id) ours = onlies[guild] if guild in onlies else None ourchan = ours[chan_id] \ if ours is not None and chan_id in ours else None if ourchan is None: await ctx.send(':person_shrugging: None set.') return ourchan.remove(command) if len(ourchan) == 0: del ours[chan_id] else: ours[chan_id] = ourchan if len(ours) == 0: del onlies[guild] else: onlies[guild] = ours log.info(f'{ctx.author} removed {command} from {channel} whitelist') await ctx.send(':wastebasket: Removed.')
async def list(self, ctx, server: typing.Optional[bool] = False): "List all current channel's whitelisted commands" guild = str(ctx.guild.id) channel = str(ctx.channel.id) if guild not in onlies: onlies[guild] = dict() ours = onlies[guild] if channel not in ours: ours[channel] = set([]) output = '**, **'.join(ours[channel]) if not len(output): output = 'None' log.info(f'{ctx.author} viewed command whitelist for {ctx.channel}') await ctx.send(f':guard: **{output}**')
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 add(self, ctx, command: str, *, params: channel_arg = channel_arg.defaults()): """ Add the given command to the Only whitelist Enables <command> in this channel. Use the 'channel' parameter to specify a channel other than the current one. Example: only.add test channel=#lobby """ channel = params['channel'] if 'channel' in params else ctx.channel chan_id = str(channel.id) guild = str(ctx.guild.id) if guild not in onlies: onlies[guild] = dict() ours = onlies[guild] if chan_id not in ours: ours[chan_id] = set([]) ourchan = ours[chan_id] if command in ourchan: await ctx.send(f':newspaper: Already done.') return ourchan.add(command) ours[chan_id] = ourchan onlies[guild] = ours log.info(f'{ctx.author} added {command} to {channel} whitelist') await ctx.send(f':shield: Done.')
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 get(self, ctx, name: Optional[str] = None, *, params: channel_arg = channel_arg.defaults()): """ View a setting's value Use the 'channel' parameter to specify a channel other than the current one. Example: get key channel=#lobby """ 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 channel = ctx.channel if 'channel' not in params else params['channel'] val = settings[name].get(ctx, channel=channel) 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} in {channel}')