diff --git a/clients/constants.py b/clients/constants.py index 5554aa7..815a9ce 100644 --- a/clients/constants.py +++ b/clients/constants.py @@ -26,10 +26,11 @@ class ACTION(IntEnum): ATTR_PRETTY = { "crime_coefficient_100": "crime coefficient >= 100", "crime_coefficient_300": "crime coefficient >= 300", - ATTRIBUTE.TOXICITY: "toxicity", - ATTRIBUTE.SEVERE_TOXICITY: "severe toxicity", - ATTRIBUTE.IDENTITY_ATTACK: "identity attack", - ATTRIBUTE.INSULT: "insult", - ATTRIBUTE.PROFANITY: "profanity", - ATTRIBUTE.SEXUALLY_EXPLICIT: "sexually explicit", + ATTRIBUTE.TOXICITY.value: "toxicity", + ATTRIBUTE.SEVERE_TOXICITY.value: "severe toxicity", + ATTRIBUTE.IDENTITY_ATTACK.value: "identity attack", + ATTRIBUTE.INSULT.value: "insult", + ATTRIBUTE.THREAT.value: "threat", + ATTRIBUTE.PROFANITY.value: "profanity", + ATTRIBUTE.SEXUALLY_EXPLICIT.value: "sexually explicit", } diff --git a/commands/dominator.py b/commands/dominator.py new file mode 100644 index 0000000..1d02b99 --- /dev/null +++ b/commands/dominator.py @@ -0,0 +1,151 @@ +from typing import Union +from loguru import logger as log +from discord import ( + ApplicationContext, + Bot, + Cog, + SlashCommandGroup, + option, + SlashCommandOptionType, + OptionChoice, +) + +from clients.backend.dominator.member_dominators import MemberDominators +from clients.backend.dominator.message_dominators import MessageDominators +from clients.constants import ACTION, ATTR_PRETTY, ATTRIBUTE +from embeds.dominator import embed_dominator + + +class Dominator(Cog): + + base_dominator = SlashCommandGroup( + "dominator", description="configure sibyl's automod capabilities" + ) + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @base_dominator.command(description="configure message dominator") + @option( + "attribute", + type=SlashCommandOptionType.string, + description="attribute to set", + choices=list( + map( + lambda attr: OptionChoice(name=ATTR_PRETTY[attr.value], value=attr), + ATTRIBUTE.__members__.values(), + ), + ), + ) + @option( + "action", + type=ACTION, + description="action to take upon exceeding threshold", + required=False, + ) + @option( + "threshold", + type=SlashCommandOptionType.number, + description="threshold for attribute score", + required=False, + min_value=0, + max_value=1, + ) + async def message( + self, + ctx: ApplicationContext, + attribute: str, + action: ACTION, + threshold: float, + ) -> None: + if not ctx.author.guild_permissions.administrator: + await ctx.followup.send( + "you do not have permissions to configure notification settings" + ) + return + await ctx.defer() + + await self.configure_dominator( + ctx, MessageDominators, attribute, action, threshold + ) + + @base_dominator.command(description="configure member dominator") + @option( + "attribute", + type=SlashCommandOptionType.string, + description="attribute to set", + choices=list( + map( + lambda attr: OptionChoice(name=ATTR_PRETTY[attr], value=attr), + ATTR_PRETTY.keys(), + ) + ), + ) + @option( + "action", + type=ACTION, + description="action to take upon exceeding threshold", + required=False, + ) + @option( + "threshold", + type=SlashCommandOptionType.number, + description="threshold for attribute score", + required=False, + min_value=0, + max_value=1, + ) + async def member( + self, + ctx: ApplicationContext, + attribute: str, + action: ACTION, + threshold: float, + ) -> None: + if not ctx.author.guild_permissions.administrator: + await ctx.followup.send( + "you do not have permissions to configure notification settings" + ) + return + await ctx.defer() + + await self.configure_dominator( + ctx, MemberDominators, attribute, action, threshold + ) + + async def configure_dominator( + self, + ctx: ApplicationContext, + dominator: Union[MessageDominators, MemberDominators], + attribute: str, + action: ACTION, + threshold: float, + ) -> None: + if attribute == "crime_coefficient_100" and action is not None: + dominator.update( + {"communityID": ctx.guild_id, "crime_coefficient_100_action": action} + ) + elif attribute == "crime_coefficient_300" and action is not None: + dominator.update( + {"communityID": ctx.guild_id, "crime_coefficient_300_action": action} + ) + else: + trigger_data = {"communityID": ctx.guild_id} + if action is not None: + trigger_data[f"{attribute}_action"] = action + if threshold is not None: + trigger_data[f"{attribute}_threshold"] = threshold + + dominator.update(trigger_data) + + await ctx.edit( + embed=embed_dominator(dominator.read(ctx.guild_id), attribute, ctx.guild) + ) + + log.info( + "{} trigger has been successfully updated for {} in server: {} ({})", + attribute, + dominator.__class__.__name__, + ctx.guild.name, + ctx.guild_id, + ) diff --git a/commands/psychopass.py b/commands/psychopass.py new file mode 100644 index 0000000..2dce569 --- /dev/null +++ b/commands/psychopass.py @@ -0,0 +1,53 @@ +from loguru import logger as log +from discord import ( + ApplicationContext, + Bot, + Cog, + slash_command, + User, + SlashCommandOptionType, + option, +) + +from clients.backend.psychopass.community_psycho_passes import CommmunityPsychoPasses +from clients.backend.psychopass.psycho_passes import PsychoPasses +from embeds.psycho_pass import embed_community_psycho_pass, embed_psycho_pass + + +class PsychoPass(Cog): + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @slash_command(description="get the psycho-pass of a user or server") + @option( + "user", + type=SlashCommandOptionType.user, + description="gets this user's psycho-pass", + required=False, + ) + async def psychopass(self, ctx: ApplicationContext, user: User) -> None: + log.debug("{} /psychopass user: @{}", ctx.guild_id, user) + await ctx.defer() + if user is not None: + log.info( + "@{} ({}) has requested the psycho-pass of @{} ({})", + ctx.user.name, + ctx.user.id, + user.name, + user.id, + ) + psycho_pass = PsychoPasses.read(user.id) + await ctx.edit(embed=embed_psycho_pass(psycho_pass, ctx.user, user)) + else: + log.info( + "@{} ({}) has requested the area stress level of {} ({})", + ctx.user.name, + ctx.user.id, + ctx.guild.name, + ctx.guild_id, + ) + psycho_pass = CommmunityPsychoPasses.read(ctx.guild_id) + await ctx.edit( + embed=embed_community_psycho_pass(psycho_pass, ctx.user, ctx.guild) + ) diff --git a/embeds/dominator.py b/embeds/dominator.py new file mode 100644 index 0000000..5c655de --- /dev/null +++ b/embeds/dominator.py @@ -0,0 +1,35 @@ +from discord import Embed, EmbedAuthor, Guild + +from clients.constants import ATTR_PRETTY, ATTRIBUTE, ACTION + + +def embed_dominator(dominator: dict, attribute: str, server: Guild) -> Embed: + embed = Embed( + description=f"{ATTR_PRETTY[attribute]} updated", + author=EmbedAuthor(name="sibylmod"), + thumbnail=server.icon.url, + ) + + if ( + "crime_coefficient_100_action" in dominator.keys() + and "crime_coefficient_300_action" in dominator.keys() + ): + embed.title = f"member dominator settings for server: {server.name}" + embed.add_field( + name=f"{ATTR_PRETTY['crime_coefficient_100']} action", + value=ACTION(dominator["crime_coefficient_100_action"]).name, + ) + embed.add_field( + name=f"{ATTR_PRETTY['crime_coefficient_300']} action", + value=ACTION(dominator["crime_coefficient_300_action"]).name, + ) + else: + embed.title = f"message dominator settings for server: {server.name}" + + for attribute in ATTRIBUTE.__members__.values(): + embed.add_field( + name=f"{ATTR_PRETTY[attribute]} action / threshold", + value=f"{ACTION(dominator[f'{attribute}_action']).name} / {dominator[f'{attribute}_threshold']}", + ) + + return embed diff --git a/embeds/member_moderation.py b/embeds/member_moderation.py deleted file mode 100644 index df3dc86..0000000 --- a/embeds/member_moderation.py +++ /dev/null @@ -1,31 +0,0 @@ -from datetime import datetime, UTC -from discord import Embed, EmbedAuthor, EmbedFooter, Member -from clients.constants import ACTION, ATTR_PRETTY - - -def embed_member_moderation( - member: Member, action: ACTION, reasons: list[tuple] -) -> Embed: - embed = Embed( - title=f"flagged psycho-pass of {member.name}", - author=EmbedAuthor(name="sibylmod", icon_url=member.avatar.url), - timestamp=datetime.now(UTC), - footer=EmbedFooter(text=member.id), - ) - - if action is ACTION.BAN: - embed.description = f"banned {member.mention}" - elif action is ACTION.KICK: - embed.description = f"kicked {member.mention}" - elif action is ACTION.MUTE: - embed.description = f"muted {member.mention}" - elif action >= ACTION.NOTIFY: - embed.description = "notified moderators" - - for reason in reasons: - attribute, score, threshold = reason - embed.add_field(name="\u200B", value="\u200B") - embed.add_field(name=ATTR_PRETTY[attribute], value=score, inline=True) - embed.add_field(name="threshold", value=threshold, inline=True) - - return embed diff --git a/embeds/message_moderation.py b/embeds/message_moderation.py deleted file mode 100644 index 444cd00..0000000 --- a/embeds/message_moderation.py +++ /dev/null @@ -1,34 +0,0 @@ -from datetime import datetime, UTC -from discord import Embed, EmbedAuthor, EmbedFooter, Message -from clients.constants import ACTION, ATTR_PRETTY - - -def embed_message_moderation( - message: Message, action: ACTION, reasons: list[tuple] -) -> Embed: - embed = Embed( - title=f"flagged message from {message.author.name}", - url=message.jump_url, - author=EmbedAuthor(name="sibylmod", icon_url=message.author.avatar.url), - timestamp=datetime.now(UTC), - footer=EmbedFooter(text=message.id), - ) - - if action is ACTION.BAN: - embed.description = f"banned {message.author.mention}" - elif action is ACTION.KICK: - embed.description = f"kicked {message.author.mention}" - elif action is ACTION.MUTE: - embed.description = f"muted {message.author.mention}" - elif action is ACTION.REMOVE: - embed.description = "removed message" - elif action is ACTION.NOTIFY: - embed.description = "notified moderators" - - for reason in reasons: - attribute, score, threshold = reason - embed.add_field(name="\u200B", value="\u200B") - embed.add_field(name=ATTR_PRETTY[attribute], value=score, inline=True) - embed.add_field(name="threshold", value=threshold, inline=True) - - return embed diff --git a/embeds/moderation.py b/embeds/moderation.py new file mode 100644 index 0000000..5e95180 --- /dev/null +++ b/embeds/moderation.py @@ -0,0 +1,62 @@ +from datetime import datetime, UTC +from discord import Embed, EmbedAuthor, EmbedFooter, Message, Member +from clients.constants import ACTION, ATTR_PRETTY + + +def embed_message_moderation( + message: Message, action: ACTION, reasons: list[tuple] +) -> Embed: + embed = Embed( + title=f"flagged message from {message.author.name}", + url=message.jump_url, + author=EmbedAuthor(name="sibylmod", icon_url=message.author.avatar.url), + timestamp=datetime.now(UTC), + footer=EmbedFooter(text=message.id), + ) + + if action is ACTION.BAN: + embed.description = f"banned {message.author.mention}" + elif action is ACTION.KICK: + embed.description = f"kicked {message.author.mention}" + elif action is ACTION.MUTE: + embed.description = f"muted {message.author.mention}" + elif action is ACTION.REMOVE: + embed.description = "removed message" + elif action is ACTION.NOTIFY: + embed.description = "notified moderators" + + for reason in reasons: + attribute, score, threshold = reason + embed.add_field( + name=f"{ATTR_PRETTY[attribute]} / threshold", value=f"{score} / {threshold}" + ) + + return embed + + +def embed_member_moderation( + member: Member, action: ACTION, reasons: list[tuple] +) -> Embed: + embed = Embed( + title=f"flagged psycho-pass of {member.name}", + author=EmbedAuthor(name="sibylmod", icon_url=member.avatar.url), + timestamp=datetime.now(UTC), + footer=EmbedFooter(text=member.id), + ) + + if action is ACTION.BAN: + embed.description = f"banned {member.mention}" + elif action is ACTION.KICK: + embed.description = f"kicked {member.mention}" + elif action is ACTION.MUTE: + embed.description = f"muted {member.mention}" + elif action >= ACTION.NOTIFY: + embed.description = "notified moderators" + + for reason in reasons: + attribute, score, threshold = reason + embed.add_field( + name=f"{ATTR_PRETTY[attribute]} / threshold", value=f"{score} / {threshold}" + ) + + return embed diff --git a/embeds/psycho_pass.py b/embeds/psycho_pass.py new file mode 100644 index 0000000..e32ed0a --- /dev/null +++ b/embeds/psycho_pass.py @@ -0,0 +1,71 @@ +from discord import Embed, EmbedAuthor, User, Guild +from clients.constants import ATTRIBUTE + + +def embed_psycho_pass(psycho_pass: dict, requester: User, target: User) -> Embed: + embed = Embed( + title=f"psycho-pass of {target.name}", + author=EmbedAuthor(name="sibylmod", icon_url=requester.avatar.url), + description=f"requested by {requester.mention}", + thumbnail=target.avatar.url, + color=int(psycho_pass["hue"], 16), + ) + + embed.add_field(name="crime coefficient", value=psycho_pass["crime_coefficient"]) + embed.add_field(name=ATTRIBUTE.TOXICITY, value=psycho_pass[ATTRIBUTE.TOXICITY]) + embed.add_field(name="hue", value=f"#{psycho_pass["hue"]}") + + embed.add_field( + name=ATTRIBUTE.SEVERE_TOXICITY, value=psycho_pass[ATTRIBUTE.SEVERE_TOXICITY] + ) + embed.add_field( + name=ATTRIBUTE.IDENTITY_ATTACK, value=psycho_pass[ATTRIBUTE.IDENTITY_ATTACK] + ) + embed.add_field(name=ATTRIBUTE.INSULT, value=psycho_pass[ATTRIBUTE.INSULT]) + embed.add_field(name=ATTRIBUTE.THREAT, value=psycho_pass[ATTRIBUTE.THREAT]) + embed.add_field(name=ATTRIBUTE.PROFANITY, value=psycho_pass[ATTRIBUTE.PROFANITY]) + embed.add_field( + name=ATTRIBUTE.SEXUALLY_EXPLICIT, value=psycho_pass[ATTRIBUTE.SEXUALLY_EXPLICIT] + ) + + return embed + + +def embed_community_psycho_pass( + psycho_pass: dict, requester: User, server: Guild +) -> Embed: + embed = Embed( + title=f"psycho-pass of server: {server.name}", + author=EmbedAuthor(name="sibylmod", icon_url=requester.avatar.url), + description=f"requested by {requester.mention}", + thumbnail=server.icon.url, + ) + + embed.add_field( + name=ATTRIBUTE.TOXICITY, + value=psycho_pass["area_stress_level"][ATTRIBUTE.TOXICITY], + ) + embed.add_field( + name=ATTRIBUTE.SEVERE_TOXICITY, + value=psycho_pass["area_stress_level"][ATTRIBUTE.SEVERE_TOXICITY], + ) + embed.add_field( + name=ATTRIBUTE.IDENTITY_ATTACK, + value=psycho_pass["area_stress_level"][ATTRIBUTE.IDENTITY_ATTACK], + ) + embed.add_field( + name=ATTRIBUTE.INSULT, value=psycho_pass["area_stress_level"][ATTRIBUTE.INSULT] + ) + embed.add_field( + name=ATTRIBUTE.THREAT, value=psycho_pass["area_stress_level"][ATTRIBUTE.THREAT] + ) + embed.add_field( + name=ATTRIBUTE.PROFANITY, + value=psycho_pass["area_stress_level"][ATTRIBUTE.PROFANITY], + ) + embed.add_field( + name=ATTRIBUTE.SEXUALLY_EXPLICIT, + value=psycho_pass["area_stress_level"][ATTRIBUTE.SEXUALLY_EXPLICIT], + ) + + return embed diff --git a/events/member.py b/events/member.py index f68653f..2616093 100644 --- a/events/member.py +++ b/events/member.py @@ -23,7 +23,7 @@ async def on_member_join(self, member: Member) -> None: @Cog.listener() async def on_member_remove(self, member: Member) -> None: log.info( - "@{} ({}) has left Server: {} ({})", + "@{} ({}) has left server: {} ({})", member.name, member.id, member.guild.name, diff --git a/events/message.py b/events/message.py index bcbb449..66ba0f7 100644 --- a/events/message.py +++ b/events/message.py @@ -10,11 +10,11 @@ def __init__(self, bot: Bot) -> None: @Cog.listener() async def on_message(self, message: Message) -> None: - if message.author.bot: + if message.author.bot or message.channel.nsfw or not message: return log.info( - "`@{} ({}) has sent a new message in Server: {} ({}) in Channel: {} ({})", + "`@{} ({}) has sent a new message in server: {} ({}) in channel: {} ({})", message.author.name, message.author.id, message.guild.name, @@ -27,11 +27,11 @@ async def on_message(self, message: Message) -> None: @Cog.listener() async def on_message_edit(self, _before: Message, after: Message) -> None: - if after.author.bot or after.channel.is_nsfw or after.content != "": + if after.author.bot or after.channel.nsfw or not after: return log.info( - "`@{} ({}) has edited a message in Server: {} ({}) in Channel: {} ({})", + "`@{} ({}) has edited a message in server: {} ({}) in channel: {} ({})", after.author.name, after.author.id, after.guild.name, diff --git a/events/moderation.py b/events/moderation.py index ecbb69b..e8acf07 100644 --- a/events/moderation.py +++ b/events/moderation.py @@ -8,8 +8,7 @@ from clients.backend.psychopass.psycho_passes import PsychoPasses from clients.constants import ACTION, ATTRIBUTE, DEFAULT_TIMEOUT from clients.perspective_api import analyze_comment -from embeds.member_moderation import embed_member_moderation -from embeds.message_moderation import embed_message_moderation +from embeds.moderation import embed_message_moderation, embed_member_moderation async def moderate_member(member: Member) -> None: @@ -135,4 +134,14 @@ async def moderate_message(message: Message) -> None: until=datetime.now(UTC) + DEFAULT_TIMEOUT, reason=reason ) + log.info( + "{} has been taken on @{} ({}) in server: {} ({}) because of {}", + max_action.name, + message.author.name, + message.author.id, + message.guild.name, + message.guild.id, + reason, + ) + await moderate_member(message.author) diff --git a/sibyl-discord.py b/sibyl-discord.py index 09cfd36..13eb74f 100644 --- a/sibyl-discord.py +++ b/sibyl-discord.py @@ -3,6 +3,8 @@ from discord import Bot, Intents from commands.sibyl import Sibyl +from commands.psychopass import PsychoPass +from commands.dominator import Dominator from events.ready import Ready from events.guild import GuildEvents from events.member import MemberEvents @@ -18,7 +20,7 @@ bot = Bot(intents=intents, debug_guilds=[1063590532711972945]) -cogs = [Sibyl, Ready, GuildEvents, MemberEvents, MessageEvents] +cogs = [Sibyl, PsychoPass, Dominator, Ready, GuildEvents, MemberEvents, MessageEvents] for cog in cogs: bot.add_cog(cog(bot)) log.debug("registered cog: {}", cog.__name__)