async def _command(self, ctx: commands.Context, toggle: bool = None): # noqa if toggle is None: # noinspection PyTypeChecker toggle = not await DummyModule().config.guild(ctx.guild ).ignore.guild() # noinspection PyTypeChecker await DummyModule().config.guild(ctx.guild ).ignore.guild.set(toggle) await ctx.send( tick( i18n("Now ignoring the current server") if toggle else i18n("No longer ignoring the current server")))
async def join(self, member: discord.Member): return ((LogEntry( self, colour=discord.Color.green(), require_fields=False, description=i18n( "Member {} joined\n\nAccount was created {}").format( member.mention, td_format(member.created_at - datetime.utcnow(), append_str=True), ), ).set_author(name=i18n("Member Joined"), icon_url=self.icon_uri(member)).set_footer( text=i18n("Member ID: {}").format(member.id))) if await self.is_opt_enabled("join") else None)
async def delete(self, message: discord.Message): return (( LogEntry(self, colour=discord.Colour.red(), ignore_fields=["Message Author", "Channel"]).set_author( name=i18n("Message Deleted"), icon_url=self.icon_uri(message.author)).set_footer( text=i18n("Message ID: {}").format(message.id)). add_field( name=i18n("Message Author"), inline=True, value="{message.author.mention} ({message.author.id})".format( message=message), ).add_field( name=i18n("Channel"), inline=True, value="{message.channel.mention} ({message.channel.id})". format(message=message), ).add_field( name=i18n("Content"), value=message.content or inline(i18n("No message content")), ) # due to how LogEntry.add_field works, this will only display if value is not None .add_field( name=i18n("Attachments"), value=("\n".join([f"<{x.url}>" for x in message.attachments]) if message.attachments else None), )) if all([ await self.is_opt_enabled("delete"), not message.author.bot ]) else None)
async def update(self, before: discord.Role, after: discord.Role): embed = LogEntry( self, colour=discord.Colour.blurple(), description=i18n("Role: {}").format(after.mention), ) embed.set_author(name=i18n("Role Updated"), icon_url=self.icon_uri()) embed.set_footer(text=i18n("Role ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ {"name": i18n("Name"), "value": "name", "config_opt": ("update", "name")}, { "name": i18n("Mentionable"), "value": "mentionable", "config_opt": ("update", "mentionable"), }, {"name": i18n("Hoist"), "value": "hoist", "config_opt": ("update", "hoist")}, { "name": i18n("Colour"), "value": "colour", "converter": lambda x: ( str(x) if x != discord.Colour.default() else i18n("None") ), "config_opt": ("update", "colour"), }, { "name": i18n("Permissions"), "value": "permissions", "diff": True, "converter": lambda x: [ str(permissions.get(name, name)) for name, val in x if val ], "config_opt": ("update", "permissions"), }, { "name": i18n("Position"), "value": "position", "config_opt": ("update", "position"), }, ], )
async def logset_modules(self, ctx: commands.Context): """List all available modules""" modules = [ "{} \N{EM DASH} {}".format(bold(module.friendly_name), inline(str(module.name))) for module in await retrieve_all_modules(ctx) ] await ctx.send( info( i18n("Available modules:\n\n{modules}").format( modules="\n".join(modules))))
class VoiceModule(Module): name = "voice" friendly_name = i18n("Voice") description = i18n("Voice status logging") settings = { "channel": i18n("Channel joining, leaving, and switching"), "mute": { "self": i18n("Self mute"), "server": i18n("Server mute") }, "deaf": { "self": i18n("Self deaf"), "server": i18n("Server deaf") }, } async def update(self, before: discord.VoiceState, after: discord.VoiceState, member: discord.Member): embed = LogEntry(self, colour=discord.Colour.greyple()) embed.set_author(name="Member Voice State Updated", icon_url=self.icon_uri(member)) embed.description = i18n("Member: {}").format(member.mention) embed.set_footer(text=i18n("Member ID: {}").format(member.id)) return await embed.add_multiple_changed( before, after, [ { "name": i18n("Channel"), "value": "channel", "config_opt": ("channel", ) }, { "name": i18n("Self Mute"), "value": "self_mute", "config_opt": ("mute", "self") }, { "name": i18n("Server Mute"), "value": "mute", "config_opt": ("mute", "server") }, { "name": i18n("Self Deaf"), "value": "self_deaf", "config_opt": ("deaf", "self") }, { "name": i18n("Server Deaf"), "value": "deaf", "config_opt": ("deaf", "server") }, ], )
async def logset_module(self, ctx: commands.Context, module: str, *settings: str): """Get or set a module's settings""" module = await retrieve_module(ctx, module) if not settings: await ctx.send(embed=await module.config_embed()) else: await module.toggle_options(*settings) await ctx.send( content=tick( i18n("Updated settings for module **{}**").format( module.friendly_name)), embed=await module.config_embed(), )
async def update(self, before: discord.Member, after: discord.Member): embed = LogEntry( self, colour=discord.Color.blurple(), description=i18n("Member: {}").format(after.mention), ) embed.set_author(name=i18n("Member Updated"), icon_url=self.icon_uri(after)) embed.set_footer(text=i18n("Member ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ { "name": i18n("Username"), "value": "name", "config_opt": ["update", "name"] }, { "name": i18n("Discriminator"), "value": "discriminator", "config_opt": ["update", "discriminator"], }, { "name": i18n("Nickname"), "value": "nick", "converter": lambda x: x or inline(i18n("None")), "config_opt": ["update", "nickname"], }, { "name": i18n("Roles"), "value": "roles", "diff": True, "converter": lambda x: [str(y) for y in reversed(x) if not y.is_default()], "config_opt": ["update", "roles"], }, ], )
async def update(self, before: discord.VoiceState, after: discord.VoiceState, member: discord.Member): embed = LogEntry(self, colour=discord.Colour.greyple()) embed.set_author(name="Member Voice State Updated", icon_url=self.icon_uri(member)) embed.description = i18n("Member: {}").format(member.mention) embed.set_footer(text=i18n("Member ID: {}").format(member.id)) return await embed.add_multiple_changed( before, after, [ { "name": i18n("Channel"), "value": "channel", "config_opt": ("channel", ) }, { "name": i18n("Self Mute"), "value": "self_mute", "config_opt": ("mute", "self") }, { "name": i18n("Server Mute"), "value": "mute", "config_opt": ("mute", "server") }, { "name": i18n("Self Deaf"), "value": "self_deaf", "config_opt": ("deaf", "self") }, { "name": i18n("Server Deaf"), "value": "deaf", "config_opt": ("deaf", "server") }, ], )
async def update(self, before: discord.Guild, after: discord.Guild): if any([before.unavailable, after.unavailable]): return None embed = LogEntry(self, colour=discord.Color.blurple()) embed.set_author(name=i18n("Server Updated"), icon_url=self.icon_uri()) embed.set_footer(text=i18n("Server ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ {"name": i18n("Name"), "value": "name", "config_opt": ["name"]}, { "name": i18n("Owner"), "value": "owner", "converter": lambda x: getattr(x, "mention", str(x)), "config_opt": ["owner"], }, { "name": i18n("2FA Requirement"), "value": "mfa_level", "converter": lambda x: i18n("Enabled") if x == 1 else i18n("Disabled"), "config_opt": ["2fa"], }, { "name": i18n("AFK Channel"), "value": "afk_channel", "converter": lambda x: getattr(x, "mention", i18n("None")), "config_opt": ["afk", "channel"], }, { "name": i18n("AFK Timeout"), "value": "afk_timeout", "converter": lambda x: td_format(timedelta(seconds=x)), "config_opt": ["afk", "timeout"], }, { "name": i18n("Voice Region"), "value": "region", "converter": lambda x: normalize(str(x)), "config_opt": ["region"], }, { "name": i18n("Content Filter"), "value": "explicit_content_filter", "converter": lambda x: normalize(str(x)), "config_opt": ["filter"], }, ], )
class RoleModule(Module): name = "role" friendly_name = i18n("Role") description = i18n("Role creation, deletion and update logging") settings = { "create": i18n("Role creations"), "delete": i18n("Role deletions"), "update": { "name": i18n("Role names"), "permissions": i18n("Role permissions"), "colour": i18n("Role colour"), "mentionable": i18n("If a role can be mentioned or not"), "hoist": i18n("If a role is displayed separately from online members"), "position": i18n("A roles position in the role hierarchy"), }, } async def create(self, role: discord.Role): if not await self.is_opt_enabled("create"): return None embed = ( LogEntry( self, colour=discord.Colour.green(), require_fields=False, description=i18n("Role: {}").format(role.mention), ) .set_author(name=i18n("Role Created"), icon_url=self.icon_uri()) .set_footer(text=i18n("Role ID: {}").format(role.id)) ) embed.add_field( name=i18n("Colour"), value=str(role.colour) if role.colour != discord.Colour.default() else i18n("None"), inline=True, ) embed.add_field( name=i18n("Hoisted"), value=i18n("Yes") if role.hoist else i18n("No"), inline=True ) embed.add_field( name=i18n("Mentionable"), value=i18n("Yes") if role.mentionable else i18n("No"), inline=True, ) embed.add_field( name=i18n("Permissions"), value=", ".join([str(permissions.get(x, x)) for x, y in role.permissions if y]), inline=False, ) return embed async def delete(self, role: discord.Role): if not await self.is_opt_enabled("delete"): return None return ( LogEntry( self, colour=discord.Colour.red(), require_fields=False, description=i18n("Role `{}` was deleted").format(role.name), ) .set_author(name=i18n("Role Deleted"), icon_url=self.icon_uri()) .set_footer(text=i18n("Role ID: {}").format(role.id)) ) async def update(self, before: discord.Role, after: discord.Role): embed = LogEntry( self, colour=discord.Colour.blurple(), description=i18n("Role: {}").format(after.mention), ) embed.set_author(name=i18n("Role Updated"), icon_url=self.icon_uri()) embed.set_footer(text=i18n("Role ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ {"name": i18n("Name"), "value": "name", "config_opt": ("update", "name")}, { "name": i18n("Mentionable"), "value": "mentionable", "config_opt": ("update", "mentionable"), }, {"name": i18n("Hoist"), "value": "hoist", "config_opt": ("update", "hoist")}, { "name": i18n("Colour"), "value": "colour", "converter": lambda x: ( str(x) if x != discord.Colour.default() else i18n("None") ), "config_opt": ("update", "colour"), }, { "name": i18n("Permissions"), "value": "permissions", "diff": True, "converter": lambda x: [ str(permissions.get(name, name)) for name, val in x if val ], "config_opt": ("update", "permissions"), }, { "name": i18n("Position"), "value": "position", "config_opt": ("update", "position"), }, ], )
async def create(self, role: discord.Role): if not await self.is_opt_enabled("create"): return None embed = ( LogEntry( self, colour=discord.Colour.green(), require_fields=False, description=i18n("Role: {}").format(role.mention), ) .set_author(name=i18n("Role Created"), icon_url=self.icon_uri()) .set_footer(text=i18n("Role ID: {}").format(role.id)) ) embed.add_field( name=i18n("Colour"), value=str(role.colour) if role.colour != discord.Colour.default() else i18n("None"), inline=True, ) embed.add_field( name=i18n("Hoisted"), value=i18n("Yes") if role.hoist else i18n("No"), inline=True ) embed.add_field( name=i18n("Mentionable"), value=i18n("Yes") if role.mentionable else i18n("No"), inline=True, ) embed.add_field( name=i18n("Permissions"), value=", ".join([str(permissions.get(x, x)) for x, y in role.permissions if y]), inline=False, ) return embed
class ChannelModule(Module): name = "channel" friendly_name = i18n("Channel") description = i18n("Channel creation, deletion, and update logging") settings = { "create": i18n("Channel creations"), "delete": i18n("Channel deletions"), "update": { "name": i18n("Channel name update"), "category": i18n("Channel category changes"), "position": i18n("Channel position changes"), "topic": i18n("Channel topic changes"), "bitrate": i18n("Channel bitrate changes"), "userlimit": i18n("Channel user limit changes"), }, } async def create(self, channel: discord.abc.GuildChannel): # noinspection PyUnresolvedReferences return ((LogEntry( self, colour=discord.Color.green(), require_fields=False, description=i18n("Channel {} was created").format(channel.mention), ).set_author(name=i18n("Channel Created"), icon_url=self.icon_uri()).set_footer( text=i18n("Channel ID: {}").format(channel.id))) if await self.is_opt_enabled("create") else None) async def delete(self, channel: discord.abc.GuildChannel): # noinspection PyUnresolvedReferences return ((LogEntry( self, colour=discord.Color.red(), require_fields=False, description=i18n("Channel `{}` was deleted").format( getattr(channel, "name", i18n("Unknown channel"))), ).set_author(name=i18n("Channel Deleted"), icon_url=self.icon_uri()).set_footer( text=i18n("Channel ID: {}").format(channel.id))) if await self.is_opt_enabled("delete") else None) async def update(self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel): embed = (LogEntry( self, colour=discord.Color.blurple(), description=i18n("Channel: {}").format(after.mention), ).set_footer(text=i18n("Channel ID: {}").format(getattr( after, "id"))).set_author(name=i18n("Channel Updated"), icon_url=self.icon_uri())) checks = [ { "name": i18n("Name"), # yes, pycharm, a guild channel does have a name attribute "before": getattr(before, "name"), "after": getattr(after, "name"), "config_opt": ("update", "name"), }, { "name": i18n("Category"), "before": before.category, "after": after.category, "converter": lambda x: getattr(x, "mention", i18n("None")), "config_opt": ("update", "category"), }, { "name": i18n("Position"), "before": before.position, "after": after.position, "config_opt": ("update", "position"), }, ] if isinstance(before, discord.TextChannel) and isinstance( after, discord.TextChannel): checks += [{ "name": i18n("Channel Topic"), "before": before.topic, "after": after.topic, "diff": True, "config_opt": ("update", "topic"), }] elif isinstance(before, discord.VoiceChannel) and isinstance( after, discord.VoiceChannel): checks += [ { "name": i18n("User Limit"), "before": before.user_limit, "after": after.user_limit, "config_opt": ("update", "userlimit"), }, { "name": i18n("Bitrate"), "before": before.bitrate, "after": after.bitrate, "converter": lambda x: "{} kbps".format(x[:4]), "config_opt": ("update", "bitrate"), }, ] return await embed.add_multiple_changed(before, after, checks)
async def update(self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel): embed = (LogEntry( self, colour=discord.Color.blurple(), description=i18n("Channel: {}").format(after.mention), ).set_footer(text=i18n("Channel ID: {}").format(getattr( after, "id"))).set_author(name=i18n("Channel Updated"), icon_url=self.icon_uri())) checks = [ { "name": i18n("Name"), # yes, pycharm, a guild channel does have a name attribute "before": getattr(before, "name"), "after": getattr(after, "name"), "config_opt": ("update", "name"), }, { "name": i18n("Category"), "before": before.category, "after": after.category, "converter": lambda x: getattr(x, "mention", i18n("None")), "config_opt": ("update", "category"), }, { "name": i18n("Position"), "before": before.position, "after": after.position, "config_opt": ("update", "position"), }, ] if isinstance(before, discord.TextChannel) and isinstance( after, discord.TextChannel): checks += [{ "name": i18n("Channel Topic"), "before": before.topic, "after": after.topic, "diff": True, "config_opt": ("update", "topic"), }] elif isinstance(before, discord.VoiceChannel) and isinstance( after, discord.VoiceChannel): checks += [ { "name": i18n("User Limit"), "before": before.user_limit, "after": after.user_limit, "config_opt": ("update", "userlimit"), }, { "name": i18n("Bitrate"), "before": before.bitrate, "after": after.bitrate, "converter": lambda x: "{} kbps".format(x[:4]), "config_opt": ("update", "bitrate"), }, ] return await embed.add_multiple_changed(before, after, checks)
class GuildModule(Module): name = "guild" friendly_name = i18n("Server") description = i18n("Server update logging") settings = { "2fa": i18n("Administration two-factor authentication requirement"), "afk": { "timeout": i18n("How long members can be AFK in voice"), "channel": i18n("The voice channel AFK members will be moved to"), }, "name": i18n("Guild name"), "owner": i18n("Guild ownership transfers"), "region": i18n("Voice server region"), "filter": i18n("Explicit content filter"), } async def update(self, before: discord.Guild, after: discord.Guild): if any([before.unavailable, after.unavailable]): return None embed = LogEntry(self, colour=discord.Color.blurple()) embed.set_author(name=i18n("Server Updated"), icon_url=self.icon_uri()) embed.set_footer(text=i18n("Server ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ {"name": i18n("Name"), "value": "name", "config_opt": ["name"]}, { "name": i18n("Owner"), "value": "owner", "converter": lambda x: getattr(x, "mention", str(x)), "config_opt": ["owner"], }, { "name": i18n("2FA Requirement"), "value": "mfa_level", "converter": lambda x: i18n("Enabled") if x == 1 else i18n("Disabled"), "config_opt": ["2fa"], }, { "name": i18n("AFK Channel"), "value": "afk_channel", "converter": lambda x: getattr(x, "mention", i18n("None")), "config_opt": ["afk", "channel"], }, { "name": i18n("AFK Timeout"), "value": "afk_timeout", "converter": lambda x: td_format(timedelta(seconds=x)), "config_opt": ["afk", "timeout"], }, { "name": i18n("Voice Region"), "value": "region", "converter": lambda x: normalize(str(x)), "config_opt": ["region"], }, { "name": i18n("Content Filter"), "value": "explicit_content_filter", "converter": lambda x: normalize(str(x)), "config_opt": ["filter"], }, ], )
class MessageModule(Module): name = "message" friendly_name = i18n("Message") description = i18n("Message edit and deletion logging") settings = { "edit": i18n("Message edits"), "delete": i18n("Message deletions"), "bulkdelete": i18n("Bulk message deletions"), } async def edit(self, before: discord.Message, after: discord.Message): if after.author.bot: return None embed = LogEntry(self, colour=discord.Colour.blurple(), ignore_fields=["Message Author", "Channel"]) embed.set_author(name=i18n("Message Edited"), icon_url=self.icon_uri(after.author)) embed.set_footer(text=i18n("Message ID: {}").format(after.id)) embed.add_field( name=i18n("Message Author"), inline=True, value="{after.author.mention} ({after.author.id})".format( after=after), ) embed.add_field( name=i18n("Channel"), inline=True, value="{after.channel.mention} ({after.channel.id})".format( after=after), ) await embed.add_if_changed( name=i18n("Content Diff"), before=before.content, after=after.content, diff=True, config_opt=["edit"], ) return embed async def delete(self, message: discord.Message): return (( LogEntry(self, colour=discord.Colour.red(), ignore_fields=["Message Author", "Channel"]).set_author( name=i18n("Message Deleted"), icon_url=self.icon_uri(message.author)).set_footer( text=i18n("Message ID: {}").format(message.id)). add_field( name=i18n("Message Author"), inline=True, value="{message.author.mention} ({message.author.id})".format( message=message), ).add_field( name=i18n("Channel"), inline=True, value="{message.channel.mention} ({message.channel.id})". format(message=message), ).add_field( name=i18n("Content"), value=message.content or inline(i18n("No message content")), ) # due to how LogEntry.add_field works, this will only display if value is not None .add_field( name=i18n("Attachments"), value=("\n".join([f"<{x.url}>" for x in message.attachments]) if message.attachments else None), )) if all([ await self.is_opt_enabled("delete"), not message.author.bot ]) else None) async def bulk_delete(self, channel: discord.TextChannel, message_ids: List[int]): if not message_ids: return None return ((LogEntry( self, colour=discord.Colour.dark_red(), description=i18n("{count} messages were deleted from {channel}"). format(count=len(message_ids), channel=channel.mention), require_fields=False, ).set_author(name=i18n("Message Bulk Deletion"), icon_url=self.icon_uri())) if await self.is_opt_enabled("bulkdelete") else None)
class MemberModule(Module): name = "member" friendly_name = i18n("Member") description = i18n("Member joining, leaving, and update logging") settings = { "join": i18n("Member joining"), "leave": i18n("Member leaving"), "update": { "name": i18n("Member username changes"), "discriminator": i18n("Member discriminator changes"), "nickname": i18n("Member nickname changes"), "roles": i18n("Member role changes"), }, } @classmethod def register(cls): pass @classmethod def unregister(cls): pass async def join(self, member: discord.Member): return ((LogEntry( self, colour=discord.Color.green(), require_fields=False, description=i18n( "Member {} joined\n\nAccount was created {}").format( member.mention, td_format(member.created_at - datetime.utcnow(), append_str=True), ), ).set_author(name=i18n("Member Joined"), icon_url=self.icon_uri(member)).set_footer( text=i18n("Member ID: {}").format(member.id))) if await self.is_opt_enabled("join") else None) async def leave(self, member: discord.Member): return ((LogEntry( self, colour=discord.Color.red(), require_fields=False, description=i18n("Member {} left").format(member.mention), ).set_author(name=i18n("Member Left"), icon_url=self.icon_uri(member)).set_footer( text=i18n("Member ID: {}").format(member.id))) if await self.is_opt_enabled("leave") else None) async def update(self, before: discord.Member, after: discord.Member): embed = LogEntry( self, colour=discord.Color.blurple(), description=i18n("Member: {}").format(after.mention), ) embed.set_author(name=i18n("Member Updated"), icon_url=self.icon_uri(after)) embed.set_footer(text=i18n("Member ID: {}").format(after.id)) return await embed.add_multiple_changed( before, after, [ { "name": i18n("Username"), "value": "name", "config_opt": ["update", "name"] }, { "name": i18n("Discriminator"), "value": "discriminator", "config_opt": ["update", "discriminator"], }, { "name": i18n("Nickname"), "value": "nick", "converter": lambda x: x or inline(i18n("None")), "config_opt": ["update", "nickname"], }, { "name": i18n("Roles"), "value": "roles", "diff": True, "converter": lambda x: [str(y) for y in reversed(x) if not y.is_default()], "config_opt": ["update", "roles"], }, ], )
async def logset_setup(self, ctx: commands.Context): """Quick-start for logging settings""" modules = await retrieve_all_modules(ctx) async def converter(pg: Page): return (discord.Embed(description=i18n( "**{mod.friendly_name} Settings**\nLogging to: {destination}\n\n" "\N{GEAR} \N{EM DASH} Update module log settings\n" "\N{HEADPHONE} \N{EM DASH} Set log channel\n" "\N{CROSS MARK} \N{EM DASH} Close menu").format( mod=pg.data, destination=getattr(await pg.data.log_destination( ), "mention", i18n("No log channel setup")), )).set_author( name="Logs Setup", icon_url=ctx.guild.icon_url).set_footer(text=i18n( "Module {0.current} out of {0.total}").format(pg))) last_page = 0 while True: result = await PaginatedMenu( ctx=ctx, actions={ "settings": "\N{GEAR}", "channel": "\N{HEADPHONE}", "close": "\N{CROSS MARK}", }, pages=modules, converter=converter, wrap_around=True, page=last_page, ).prompt(post_action=PostAction.NO_ACTION) module = result.page last_page = modules.index(module) try: await result.menu.message.delete() result.menu.message = None except (AttributeError, discord.HTTPException): pass if result.timed_out or result == "close": break if result == "settings": tmp = await ctx.send(embed=await module.config_embed()) resp = await prompt( ctx, content=i18n( "Please respond with a space-separated list of options you would like " "to enable."), delete_messages=True, timeout=90, ) if resp: await module.toggle_options(*str(resp.content).split(" ")) await tmp.delete() elif result == "channel": try: channel = commands.TextChannelConverter().convert( ctx, (await prompt( ctx, content=i18n( "What channel would you like to log to?"), timeout=90, delete_messages=True, )).content, ) except (commands.BadArgument, AttributeError): continue else: if channel is None: continue await module.module_config.set_raw("_log_channel", value=getattr( channel, "id", None))