From 328aa21a1f1320b407d36e29c9de7434f580b7e8 Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Mon, 5 May 2025 21:26:38 -0700 Subject: [PATCH 1/7] add basic reporting flow, sends to mod channel with color coded priority --- DiscordBot/bot.py | 25 +------ DiscordBot/report.py | 173 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 149 insertions(+), 49 deletions(-) diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index ec5dddb6..f7370511 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -6,7 +6,7 @@ import logging import re import requests -from report import Report +from report import Report, AbuseType, MisinfoCategory, HealthCategory, NewsCategory import pdb # Set up logging to the console @@ -103,29 +103,6 @@ async def handle_channel_message(self, message): if not message.channel.name == f'group-{self.group_num}': return - # Forward the message to the mod channel - mod_channel = self.mod_channels[message.guild.id] - await mod_channel.send(f'Forwarded message:\n{message.author.name}: "{message.content}"') - scores = self.eval_text(message.content) - await mod_channel.send(self.code_format(scores)) - - - def eval_text(self, message): - '''' - TODO: Once you know how you want to evaluate messages in your channel, - insert your code here! This will primarily be used in Milestone 3. - ''' - return message - - - def code_format(self, text): - '''' - TODO: Once you know how you want to show that a message has been - evaluated, insert your code here for formatting the string to be - shown in the mod channel. - ''' - return "Evaluated: '" + text+ "'" - client = ModBot() client.run(discord_token) \ No newline at end of file diff --git a/DiscordBot/report.py b/DiscordBot/report.py index d2bba994..155eecc9 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -6,8 +6,56 @@ class State(Enum): REPORT_START = auto() AWAITING_MESSAGE = auto() MESSAGE_IDENTIFIED = auto() + AWAITING_ABUSE_TYPE = auto() + AWAITING_MISINFO_CATEGORY = auto() + AWAITING_HEALTH_CATEGORY = auto() + AWAITING_NEWS_CATEGORY = auto() REPORT_COMPLETE = auto() +class AbuseType(Enum): + BULLYING = "bullying" + SUICIDE = "suicide/self-harm" + EXPLICIT = "sexually explicit/nudity" + MISINFORMATION = "misinformation" + HATE = "hate speech" + DANGER = "danger" + +SUICIDE_VARIANTS = { + "suicide", + "self harm", + "self-harm", + "selfharm", + "suicide/self harm", + "suicide/selfharm", + "suicide/self-harm", +} + +EXPLICIT_VARIANTS = { + "explicit", + "sexually explicit", + "sexual", + "nudity", + "nude", + "sexually explicit/nudity", +} + +class MisinfoCategory(Enum): + HEALTH = "health" + ADVERTISEMENT = "advertisement" + NEWS = "news" + +class HealthCategory(Enum): + EMERGENCY = "emergency" + MEDICAL_RESEARCH = "medical research" + REPRODUCTIVE = "reproductive healthcare" + TREATMENTS = "treatments" + ALTERNATIVE = "alternative medicine" + +class NewsCategory(Enum): + HISTORICAL = "historical" + POLITICAL = "political" + SCIENCE = "science" + class Report: START_KEYWORD = "report" CANCEL_KEYWORD = "cancel" @@ -17,28 +65,24 @@ def __init__(self, client): self.state = State.REPORT_START self.client = client self.message = None - - async def handle_message(self, message): - ''' - This function makes up the meat of the user-side reporting flow. It defines how we transition between states and what - prompts to offer at each of those states. You're welcome to change anything you want; this skeleton is just here to - get you started and give you a model for working with Discord. - ''' + self.abuse_type = None + self.misinfo_category = None + self.specific_category = None - if message.content == self.CANCEL_KEYWORD: + async def handle_message(self, message): + if message.content.lower() == self.CANCEL_KEYWORD: self.state = State.REPORT_COMPLETE return ["Report cancelled."] - + if self.state == State.REPORT_START: - reply = "Thank you for starting the reporting process. " + reply = "Thank you for starting the reporting process. " reply += "Say `help` at any time for more information.\n\n" reply += "Please copy paste the link to the message you want to report.\n" reply += "You can obtain this link by right-clicking the message and clicking `Copy Message Link`." self.state = State.AWAITING_MESSAGE return [reply] - + if self.state == State.AWAITING_MESSAGE: - # Parse out the three ID strings from the message link m = re.search('/(\d+)/(\d+)/(\d+)', message.content) if not m: return ["I'm sorry, I couldn't read that link. Please try again or say `cancel` to cancel."] @@ -49,24 +93,103 @@ async def handle_message(self, message): if not channel: return ["It seems this channel was deleted or never existed. Please try again or say `cancel` to cancel."] try: - message = await channel.fetch_message(int(m.group(3))) + self.message = await channel.fetch_message(int(m.group(3))) except discord.errors.NotFound: return ["It seems this message was deleted or never existed. Please try again or say `cancel` to cancel."] + + self.state = State.AWAITING_ABUSE_TYPE + reply = "What type of abuse would you like to report?\n" + reply += "• BULLYING\n" + reply += "• SUICIDE/SELF-HARM\n" + reply += "• SEXUALLY EXPLICIT/NUDITY\n" + reply += "• MISINFORMATION\n" + reply += "• HATE SPEECH\n" + reply += "• DANGER" + return ["I found this message:", "```" + self.message.author.name + ": " + self.message.content + "```", reply] - # Here we've found the message - it's up to you to decide what to do next! - self.state = State.MESSAGE_IDENTIFIED - return ["I found this message:", "```" + message.author.name + ": " + message.content + "```", \ - "This is all I know how to do right now - it's up to you to build out the rest of my reporting flow!"] - - if self.state == State.MESSAGE_IDENTIFIED: - return [""] + if self.state == State.AWAITING_ABUSE_TYPE: + abuse_type = message.content.lower() + if abuse_type in SUICIDE_VARIANTS: + self.abuse_type = AbuseType.SUICIDE + mod_channel = self.client.mod_channels[self.message.guild.id] + await mod_channel.send(f"🔴 URGENT - SUICIDE/SELF-HARM REPORT:\n{self.message.author.name}: {self.message.content}") + self.state = State.REPORT_COMPLETE + return ["Thank you for reporting. This has been escalated to our moderation team for immediate review."] - return [] + if abuse_type in EXPLICIT_VARIANTS: + self.abuse_type = AbuseType.EXPLICIT + mod_channel = self.client.mod_channels[self.message.guild.id] + await mod_channel.send(f"🔴 URGENT - EXPLICIT CONTENT REPORT:\n{self.message.author.name}: {self.message.content}") + self.state = State.REPORT_COMPLETE + return ["Thank you for reporting. This has been escalated to our moderation team for immediate review."] - def report_complete(self): - return self.state == State.REPORT_COMPLETE - + for type in AbuseType: + if abuse_type == type.value: + self.abuse_type = type + if type == AbuseType.MISINFORMATION: + self.state = State.AWAITING_MISINFO_CATEGORY + return ["Please select the misinformation category:\n• HEALTH\n• ADVERTISEMENT\n• NEWS"] + else: + mod_channel = self.client.mod_channels[self.message.guild.id] + priority = "🔴" if type in [AbuseType.HATE, AbuseType.DANGER] else "🟡" + await mod_channel.send(f"{priority} New report - {type.value.upper()}:\n{self.message.author.name}: {self.message.content}") + self.state = State.REPORT_COMPLETE + return ["Thank you for reporting, it has been sent to our moderation team."] + return ["Please select a valid abuse type from the list above."] + if self.state == State.AWAITING_MISINFO_CATEGORY: + category = message.content.lower() + for cat in MisinfoCategory: + if category == cat.value: + self.misinfo_category = cat + if cat == MisinfoCategory.HEALTH: + self.state = State.AWAITING_HEALTH_CATEGORY + return ["Please specify the health misinformation category:\n• EMERGENCY\n• MEDICAL RESEARCH\n• REPRODUCTIVE HEALTHCARE\n• TREATMENTS\n• ALTERNATIVE MEDICINE"] + elif cat == MisinfoCategory.NEWS: + self.state = State.AWAITING_NEWS_CATEGORY + return ["Please specify the news category:\n• HISTORICAL\n• POLITICAL\n• SCIENCE"] + else: # Advertisement + self.state = State.REPORT_COMPLETE + await self.client.mod_channels[self.message.guild.id].send(f"🟡 ADVERTISING MISINFO:\n{self.message.author.name}: {self.message.content}") + return ["This has been reported to our ad team."] + return ["Please select a valid misinformation category from the list above."] - + if self.state == State.AWAITING_HEALTH_CATEGORY: + health_cat = message.content.lower() + for cat in HealthCategory: + if health_cat == cat.value: + self.specific_category = cat + self.state = State.REPORT_COMPLETE + mod_channel = self.client.mod_channels[self.message.guild.id] + + if cat == HealthCategory.EMERGENCY: + await mod_channel.send(f"🔴 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") + return ["We will prioritize this and send it for review."] + elif cat in [HealthCategory.MEDICAL_RESEARCH, HealthCategory.REPRODUCTIVE]: + await mod_channel.send(f"🟡 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") + return ["This has been sent to moderators."] + else: + await mod_channel.send(f"🟢 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") + return ["This has been sent to our team. Review if necessary, marked with non-scientific flag."] + return ["Please select a valid health category from the list above."] + if self.state == State.AWAITING_NEWS_CATEGORY: + news_cat = message.content.lower() + for cat in NewsCategory: + if news_cat == cat.value: + self.specific_category = cat + self.state = State.REPORT_COMPLETE + mod_channel = self.client.mod_channels[self.message.guild.id] + + if cat == NewsCategory.POLITICAL: + await mod_channel.send(f"🟡 NEWS MISINFO:\n{self.message.author.name}: {self.message.content}") + else: + await mod_channel.send(f"🟢 NEWS MISINFO:\n{self.message.author.name}: {self.message.content}") + return ["This has been sent to our team."] + return ["Please select a valid news category from the list above."] + + return [] + + def report_complete(self): + """Returns whether the current report is in a completed state""" + return self.state == State.REPORT_COMPLETE \ No newline at end of file From d8000e3e7c9583218e8b92dd9bced6fbfa5ad157 Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Thu, 8 May 2025 18:57:23 -0700 Subject: [PATCH 2/7] remove level from user reporting, add moderator reporting flow --- DiscordBot/bot.py | 151 ++++++++++++++++++++++++++++++++++++++++++- DiscordBot/report.py | 61 ++++++++++------- 2 files changed, 188 insertions(+), 24 deletions(-) diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index f7370511..89b280c5 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -34,6 +34,7 @@ def __init__(self): self.group_num = None self.mod_channels = {} # Map from guild to the mod channel id for that guild self.reports = {} # Map from user IDs to the state of their report + self.active_mod_flow = None # State for the current moderation flow async def on_ready(self): print(f'{self.user.name} has connected to Discord! It is these guilds:') @@ -99,10 +100,156 @@ async def handle_dm(self, message): self.reports.pop(author_id) async def handle_channel_message(self, message): - # Only handle messages sent in the "group-#" channel - if not message.channel.name == f'group-{self.group_num}': + # Only handle messages sent in the "group-#-mod" channel + if message.channel.name == f'group-{self.group_num}-mod': + await self.handle_mod_channel_message(message) + elif message.channel.name == f'group-{self.group_num}': return + async def start_moderation_flow(self, report_type, report_content, message_author, message_link=None): + # Determine the initial step based on report type + if report_type.startswith('ADVERTISING MISINFO'): + initial_step = 'advertising_done' + elif report_type.startswith('MISINFORMATION') or report_type.startswith('HEALTH MISINFO') or report_type.startswith('NEWS MISINFO'): + initial_step = 'danger_level' + else: + initial_step = 'default_done' + self.active_mod_flow = { + 'step': initial_step, + 'report_type': report_type, + 'report_content': report_content, + 'message_author': message_author, + 'message_link': message_link, + 'context': {} + } + mod_channel = None + for channel in self.mod_channels.values(): + mod_channel = channel + break + if mod_channel: + await mod_channel.send(f"A new report has been submitted:\nType: {report_type}\nContent: {report_content}\nReported user: {message_author}") + if initial_step == 'danger_level': + await mod_channel.send("What is the level of danger for this report?\n• LOW\n• MEDIUM\n• HIGH") + elif initial_step == 'advertising_done': + await mod_channel.send("Report sent to advertising team. No further action required.") + self.active_mod_flow = None + elif initial_step == 'default_done': + # Just show the report, do not prompt for reply + self.active_mod_flow = None + else: + await self.prompt_next_moderation_step(mod_channel) + + async def notify_reported_user(self, user_name, guild, outcome, explanation=None): + # Find the user object by name in the guild + user = discord.utils.get(guild.members, name=user_name) + if user: + try: + msg = f"Your message was reviewed by moderators. Outcome: {outcome}." + if explanation: + msg += f"\nReason: {explanation}" + msg += "\nIf you believe this was a mistake, you may reply to this message to appeal." + await user.send(msg) + except Exception as e: + print(f"Failed to DM user {user_name}: {e}") + + async def handle_mod_channel_message(self, message): + if not self.active_mod_flow: + return + step = self.active_mod_flow['step'] + ctx = self.active_mod_flow['context'] + content = message.content.strip().lower() + mod_channel = message.channel + report_type = self.active_mod_flow['report_type'] + report_content = self.active_mod_flow['report_content'] + reported_user_name = self.active_mod_flow['message_author'] + guild = mod_channel.guild if hasattr(mod_channel, 'guild') else None + + # Misinformation moderation flow + if step == 'advertising_done': + # Already handled + self.active_mod_flow = None + return + if step == 'danger_level': + if content not in ['low', 'medium', 'high']: + await mod_channel.send("Invalid option. Please choose:\n• LOW\n• MEDIUM\n• HIGH") + return + ctx['danger_level'] = content + if content == 'low': + await mod_channel.send("Flag post as low danger. After claim is investigated, what action should be taken on post?\n• DO NOT RECOMMEND\n• FLAG AS UNPROVEN") + self.active_mod_flow['step'] = 'low_action_on_post' + return + elif content == 'medium': + await mod_channel.send("Flag post as medium danger. After claim is investigated, what action should be taken on post?\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") + self.active_mod_flow['step'] = 'medium_action_on_post' + return + elif content == 'high': + await mod_channel.send("Flag post as high danger. What emergency action should be taken based on post?\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") + self.active_mod_flow['step'] = 'high_action_on_post' + return + if step == 'low_action_on_post': + if content == 'do not recommend': + await mod_channel.send("Post will not be recommended. Action recorded. (Update algorithm so post is not recommended.)") + await self.notify_reported_user(reported_user_name, guild, outcome="Post not recommended.") + self.active_mod_flow = None + return + elif content == 'flag as unproven': + await mod_channel.send("Post will be flagged as unproven/non-scientific. Please add explanation for why post is being flagged.") + self.active_mod_flow['step'] = 'flag_explanation' + return + else: + await mod_channel.send("Invalid option. Please choose:\n• DO NOT RECOMMEND\n• FLAG AS UNPROVEN") + return + if step == 'flag_explanation': + await mod_channel.send(f"Explanation recorded: {message.content}\nFlagged post as not proven.") + await self.notify_reported_user(reported_user_name, guild, outcome="Post flagged as unproven/non-scientific.", explanation=message.content) + self.active_mod_flow = None + return + if step == 'medium_action_on_post' or step == 'high_action_on_post': + if content == 'remove': + await mod_channel.send("Post will be removed. Please add explanation for why post is being removed.") + self.active_mod_flow['step'] = 'remove_explanation' + return + elif content == 'raise': + await mod_channel.send("Raising to higher level moderator. Report sent to higher level moderators.") + self.active_mod_flow = None + return + elif content == 'report to authorities': + await mod_channel.send("Reporting to authorities. Report sent to authorities.") + self.active_mod_flow = None + return + else: + await mod_channel.send("Invalid option. Please choose:\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") + return + if step == 'remove_explanation': + await mod_channel.send(f"Explanation recorded: {message.content}\nPost removed. What action should be taken on the creator of the post?\n• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER") + ctx['remove_explanation'] = message.content + await self.notify_reported_user( + reported_user_name, + guild, + outcome="Post removed.", + explanation=ctx.get('remove_explanation', '') + ) + self.active_mod_flow['step'] = 'action_on_user' + return + if step == 'action_on_user': + if content == 'record incident': + await mod_channel.send("Incident recorded for internal use. (Add to internal incident count for user.)") + self.active_mod_flow = None + return + elif content == 'temporarily mute': + await mod_channel.send("User will be muted for 24 hours.") + self.active_mod_flow = None + return + elif content == 'remove user': + await mod_channel.send("User will be removed.") + self.active_mod_flow = None + return + else: + await mod_channel.send("Invalid option. Please choose:\n• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER") + return + + async def prompt_next_moderation_step(self, mod_channel): + await mod_channel.send("Moderator, please review the report and respond with your decision.") client = ModBot() client.run(discord_token) \ No newline at end of file diff --git a/DiscordBot/report.py b/DiscordBot/report.py index 155eecc9..fb510ab0 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -112,16 +112,26 @@ async def handle_message(self, message): if abuse_type in SUICIDE_VARIANTS: self.abuse_type = AbuseType.SUICIDE mod_channel = self.client.mod_channels[self.message.guild.id] - await mod_channel.send(f"🔴 URGENT - SUICIDE/SELF-HARM REPORT:\n{self.message.author.name}: {self.message.content}") + await mod_channel.send(f"SUICIDE/SELF-HARM REPORT:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type="SUICIDE/SELF-HARM", + report_content=self.message.content, + message_author=self.message.author.name + ) self.state = State.REPORT_COMPLETE - return ["Thank you for reporting. This has been escalated to our moderation team for immediate review."] + return ["Thank you for reporting. This has been sent to our moderation team for review."] if abuse_type in EXPLICIT_VARIANTS: self.abuse_type = AbuseType.EXPLICIT mod_channel = self.client.mod_channels[self.message.guild.id] - await mod_channel.send(f"🔴 URGENT - EXPLICIT CONTENT REPORT:\n{self.message.author.name}: {self.message.content}") + await mod_channel.send(f"EXPLICIT CONTENT REPORT:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type="EXPLICIT CONTENT", + report_content=self.message.content, + message_author=self.message.author.name + ) self.state = State.REPORT_COMPLETE - return ["Thank you for reporting. This has been escalated to our moderation team for immediate review."] + return ["Thank you for reporting. This has been sent to our moderation team for review."] for type in AbuseType: if abuse_type == type.value: @@ -131,8 +141,12 @@ async def handle_message(self, message): return ["Please select the misinformation category:\n• HEALTH\n• ADVERTISEMENT\n• NEWS"] else: mod_channel = self.client.mod_channels[self.message.guild.id] - priority = "🔴" if type in [AbuseType.HATE, AbuseType.DANGER] else "🟡" - await mod_channel.send(f"{priority} New report - {type.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await mod_channel.send(f"New report - {type.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=type.value.upper(), + report_content=self.message.content, + message_author=self.message.author.name + ) self.state = State.REPORT_COMPLETE return ["Thank you for reporting, it has been sent to our moderation team."] return ["Please select a valid abuse type from the list above."] @@ -150,7 +164,12 @@ async def handle_message(self, message): return ["Please specify the news category:\n• HISTORICAL\n• POLITICAL\n• SCIENCE"] else: # Advertisement self.state = State.REPORT_COMPLETE - await self.client.mod_channels[self.message.guild.id].send(f"🟡 ADVERTISING MISINFO:\n{self.message.author.name}: {self.message.content}") + await self.client.mod_channels[self.message.guild.id].send(f"ADVERTISING MISINFO:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type="ADVERTISING MISINFO", + report_content=self.message.content, + message_author=self.message.author.name + ) return ["This has been reported to our ad team."] return ["Please select a valid misinformation category from the list above."] @@ -161,16 +180,13 @@ async def handle_message(self, message): self.specific_category = cat self.state = State.REPORT_COMPLETE mod_channel = self.client.mod_channels[self.message.guild.id] - - if cat == HealthCategory.EMERGENCY: - await mod_channel.send(f"🔴 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") - return ["We will prioritize this and send it for review."] - elif cat in [HealthCategory.MEDICAL_RESEARCH, HealthCategory.REPRODUCTIVE]: - await mod_channel.send(f"🟡 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") - return ["This has been sent to moderators."] - else: - await mod_channel.send(f"🟢 HEALTH MISINFO:\n{self.message.author.name}: {self.message.content}") - return ["This has been sent to our team. Review if necessary, marked with non-scientific flag."] + await mod_channel.send(f"HEALTH MISINFO - {cat.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=f"HEALTH MISINFO - {cat.value.upper()}", + report_content=self.message.content, + message_author=self.message.author.name + ) + return ["This has been sent to our moderation team."] return ["Please select a valid health category from the list above."] if self.state == State.AWAITING_NEWS_CATEGORY: @@ -180,11 +196,12 @@ async def handle_message(self, message): self.specific_category = cat self.state = State.REPORT_COMPLETE mod_channel = self.client.mod_channels[self.message.guild.id] - - if cat == NewsCategory.POLITICAL: - await mod_channel.send(f"🟡 NEWS MISINFO:\n{self.message.author.name}: {self.message.content}") - else: - await mod_channel.send(f"🟢 NEWS MISINFO:\n{self.message.author.name}: {self.message.content}") + await mod_channel.send(f"NEWS MISINFO - {cat.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=f"NEWS MISINFO - {cat.value.upper()}", + report_content=self.message.content, + message_author=self.message.author.name + ) return ["This has been sent to our team."] return ["Please select a valid news category from the list above."] From 45dca1556d153a67c8bc95f78c67e4cf1eb762c7 Mon Sep 17 00:00:00 2001 From: anushehchaudry Date: Fri, 9 May 2025 01:21:24 -0700 Subject: [PATCH 3/7] Add appeals process --- DiscordBot/bot.py | 145 +++++++++++++++++++++++++++++++++++++++---- DiscordBot/report.py | 17 +++++ 2 files changed, 151 insertions(+), 11 deletions(-) diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index 89b280c5..6da724cf 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -6,7 +6,7 @@ import logging import re import requests -from report import Report, AbuseType, MisinfoCategory, HealthCategory, NewsCategory +from report import Report, AbuseType, MisinfoCategory, HealthCategory, NewsCategory, State import pdb # Set up logging to the console @@ -30,10 +30,12 @@ class ModBot(discord.Client): def __init__(self): intents = discord.Intents.default() intents.message_content = True + intents.members = True super().__init__(command_prefix='.', intents=intents) self.group_num = None self.mod_channels = {} # Map from guild to the mod channel id for that guild self.reports = {} # Map from user IDs to the state of their report + self.pending_appeals = {} self.active_mod_flow = None # State for the current moderation flow async def on_ready(self): @@ -72,6 +74,47 @@ async def on_message(self, message): await self.handle_dm(message) async def handle_dm(self, message): + if message.author.id in self.pending_appeals: + # Retrieve all pending appeals for the user + user_appeals = self.pending_appeals[message.author.id] + if not user_appeals: + return + + # Process the first pending appeal + info = user_appeals.pop(0) + if not user_appeals: + # Remove the user from pending_appeals if no appeals remain + del self.pending_appeals[message.author.id] + + mod_chan = self.mod_channels[info['guild_id']] + + # Build the appeal notice + text = ( + f"APPEAL RECEIVED:\n" + f"User: {info['reported_name']}\n" + f"Outcome: {info['outcome']}\n\n" + f"Original Message:\n{info['original_message']}" + ) + if info.get('explanation'): + text += f"\n\nReason: {info['explanation']}" + text += f"\n\nAppeal Reason:\n{message.content}" + + # Send to mod channel + await mod_chan.send(text) + + # Prompt mods for ACCEPT/UPHOLD + self.active_mod_flow = { + 'step': 'appeal_review', + 'message_author': info['reported_name'], + 'context': {}, + 'guild_id': info['guild_id'] + } + await mod_chan.send("Moderators, please respond with:\n• ACCEPT\n• UPHOLD") + + # Acknowledge to user + await message.channel.send("Your appeal has been submitted and is under review.") + return + # Handle a help message if message.content == Report.HELP_KEYWORD: reply = "Use the `report` command to begin the reporting process.\n" @@ -90,7 +133,7 @@ async def handle_dm(self, message): if author_id not in self.reports: self.reports[author_id] = Report(self) - # Let the report class handle this message; forward all the messages it returns to uss + # Let the report class handle this message; forward all the messages it returns to us responses = await self.reports[author_id].handle_message(message) for r in responses: await message.channel.send(r) @@ -139,30 +182,65 @@ async def start_moderation_flow(self, report_type, report_content, message_autho else: await self.prompt_next_moderation_step(mod_channel) - async def notify_reported_user(self, user_name, guild, outcome, explanation=None): - # Find the user object by name in the guild + async def notify_reported_user(self, user_name, guild, outcome, explanation=None, original_message=None): + """Notify the user about the outcome and provide an appeal option.""" user = discord.utils.get(guild.members, name=user_name) if user: try: msg = f"Your message was reviewed by moderators. Outcome: {outcome}." + if original_message: + msg += f"\n\n**Original Message:**\n{original_message}" if explanation: - msg += f"\nReason: {explanation}" - msg += "\nIf you believe this was a mistake, you may reply to this message to appeal." + msg += f"\n\n**Reason:** {explanation}" + msg += "\n\nIf you believe this was a mistake, you may reply to this message to appeal." await user.send(msg) except Exception as e: print(f"Failed to DM user {user_name}: {e}") + async def notify_user_of_appeal_option(self, user_name, guild, explanation): + """Notify the user about the appeal process after their post is removed.""" + user = discord.utils.get(guild.members, name=user_name) + if user: + try: + msg = f"Your post was removed for the following reason: {explanation}.\n" + msg += "If you believe this was a mistake, you can appeal by replying with your reason." + await user.send(msg) + except Exception as e: + print(f"Failed to notify user {user_name}: {e}") + async def handle_mod_channel_message(self, message): if not self.active_mod_flow: return step = self.active_mod_flow['step'] - ctx = self.active_mod_flow['context'] content = message.content.strip().lower() mod_channel = message.channel + guild = mod_channel.guild if hasattr(mod_channel, 'guild') else None + + if step == 'appeal_review': + if content == 'accept': + await mod_channel.send("The appeal has been accepted. The original decision has been overturned.") + user = discord.utils.get(guild.members, name=self.active_mod_flow['message_author']) + if user: + await user.send("Your appeal has been accepted. The original decision has been overturned.") + self.active_mod_flow = None + return + + elif content == 'uphold': + await mod_channel.send("The appeal has been reviewed and the original decision is upheld.") + user = discord.utils.get(guild.members, name=self.active_mod_flow['message_author']) + if user: + await user.send("Your appeal has been reviewed, and the original decision is upheld.") + self.active_mod_flow = None + return + + else: + await mod_channel.send("Invalid response. Please respond with:\n• ACCEPT\n• UPHOLD") + return + + ctx = self.active_mod_flow['context'] report_type = self.active_mod_flow['report_type'] report_content = self.active_mod_flow['report_content'] reported_user_name = self.active_mod_flow['message_author'] - guild = mod_channel.guild if hasattr(mod_channel, 'guild') else None # Misinformation moderation flow if step == 'advertising_done': @@ -221,13 +299,32 @@ async def handle_mod_channel_message(self, message): await mod_channel.send("Invalid option. Please choose:\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") return if step == 'remove_explanation': - await mod_channel.send(f"Explanation recorded: {message.content}\nPost removed. What action should be taken on the creator of the post?\n• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER") - ctx['remove_explanation'] = message.content + explanation = message.content + ctx['remove_explanation'] = explanation await self.notify_reported_user( reported_user_name, guild, outcome="Post removed.", - explanation=ctx.get('remove_explanation', '') + explanation=ctx.get('remove_explanation', ''), + original_message=report_content + ) + # Send only the appeal prompt + user = discord.utils.get(guild.members, name=reported_user_name) + if user: + # Track for incoming DM in pending_appeals + if user.id not in self.pending_appeals: + self.pending_appeals[user.id] = [] + self.pending_appeals[user.id].append({ + 'guild_id': guild.id, + 'reported_name': reported_user_name, + 'outcome': "Post removed.", + 'original_message': report_content, + 'explanation': explanation + }) + await mod_channel.send( + f"Explanation recorded: {explanation}\n" + "What action should be taken on the creator of the post?\n" + "• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER" ) self.active_mod_flow['step'] = 'action_on_user' return @@ -238,10 +335,36 @@ async def handle_mod_channel_message(self, message): return elif content == 'temporarily mute': await mod_channel.send("User will be muted for 24 hours.") + await self.notify_reported_user( + reported_user_name, + guild, + outcome="You have been temporarily muted.", + explanation="You violated the community guidelines.", + original_message=report_content + ) self.active_mod_flow = None return elif content == 'remove user': await mod_channel.send("User will be removed.") + await self.notify_reported_user( + reported_user_name, + guild, + outcome="You have been removed from the server.", + explanation="You violated the community guidelines.", + original_message=report_content + ) + user = discord.utils.get(guild.members, name=reported_user_name) + if user: + # Track for incoming DM in pending_appeals + if user.id not in self.pending_appeals: + self.pending_appeals[user.id] = [] + self.pending_appeals[user.id].append({ + 'guild_id': guild.id, + 'reported_name': reported_user_name, + 'outcome': "You have been removed from the server.", + 'original_message': report_content, + 'explanation': "You violated the community guidelines." + }) self.active_mod_flow = None return else: diff --git a/DiscordBot/report.py b/DiscordBot/report.py index fb510ab0..efb16d72 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -11,6 +11,8 @@ class State(Enum): AWAITING_HEALTH_CATEGORY = auto() AWAITING_NEWS_CATEGORY = auto() REPORT_COMPLETE = auto() + AWAITING_APPEAL = auto() + APPEAL_REVIEW = auto() class AbuseType(Enum): BULLYING = "bullying" @@ -207,6 +209,21 @@ async def handle_message(self, message): return [] + async def notify_reported_user(self, user_name, guild, outcome, explanation=None): + # Find the user object by name in the guild + user = discord.utils.get(guild.members, name=user_name) + if user: + try: + msg = f"Your message was reviewed by moderators. Outcome: {outcome}." + if explanation: + msg += f"\nReason: {explanation}" + msg += "\nIf you believe this was a mistake, you may reply to this message to appeal." + await user.send(msg) + if outcome == "Post removed.": + await self.notify_user_of_appeal_option(user_name, guild, explanation) + except Exception as e: + print(f"Failed to DM user {user_name}: {e}") + def report_complete(self): """Returns whether the current report is in a completed state""" return self.state == State.REPORT_COMPLETE \ No newline at end of file From 9020a0d09c5ca98d2ce5f569353999f7ec9ab6d5 Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Tue, 27 May 2025 19:20:41 -0700 Subject: [PATCH 4/7] added per-user statistics tracking in user_stats.json, changed to numerical inputs --- DiscordBot/bot.py | 112 +++++++++++++++++------ DiscordBot/report.py | 181 +++++++++++++++++++++---------------- DiscordBot/user_stats.json | 21 +++++ DiscordBot/user_stats.py | 53 +++++++++++ 4 files changed, 263 insertions(+), 104 deletions(-) create mode 100644 DiscordBot/user_stats.json create mode 100644 DiscordBot/user_stats.py diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index 89b280c5..afaa8656 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -7,6 +7,7 @@ import re import requests from report import Report, AbuseType, MisinfoCategory, HealthCategory, NewsCategory +from user_stats import UserStats import pdb # Set up logging to the console @@ -30,11 +31,13 @@ class ModBot(discord.Client): def __init__(self): intents = discord.Intents.default() intents.message_content = True + intents.members = True # Add members intent super().__init__(command_prefix='.', intents=intents) self.group_num = None self.mod_channels = {} # Map from guild to the mod channel id for that guild self.reports = {} # Map from user IDs to the state of their report self.active_mod_flow = None # State for the current moderation flow + self.user_stats = UserStats() # Initialize user statistics tracking async def on_ready(self): print(f'{self.user.name} has connected to Discord! It is these guilds:') @@ -129,7 +132,7 @@ async def start_moderation_flow(self, report_type, report_content, message_autho if mod_channel: await mod_channel.send(f"A new report has been submitted:\nType: {report_type}\nContent: {report_content}\nReported user: {message_author}") if initial_step == 'danger_level': - await mod_channel.send("What is the level of danger for this report?\n• LOW\n• MEDIUM\n• HIGH") + await mod_channel.send("What is the level of danger for this report?\n1. LOW\n2. MEDIUM\n3. HIGH") elif initial_step == 'advertising_done': await mod_channel.send("Report sent to advertising team. No further action required.") self.active_mod_flow = None @@ -164,64 +167,96 @@ async def handle_mod_channel_message(self, message): reported_user_name = self.active_mod_flow['message_author'] guild = mod_channel.guild if hasattr(mod_channel, 'guild') else None + # Get the user ID from the reported user's name + reported_user = discord.utils.get(guild.members, name=reported_user_name) + if not reported_user: + await mod_channel.send(f"Could not find user {reported_user_name}. Please verify the username is correct.") + return + # Misinformation moderation flow if step == 'advertising_done': # Already handled self.active_mod_flow = None return if step == 'danger_level': - if content not in ['low', 'medium', 'high']: - await mod_channel.send("Invalid option. Please choose:\n• LOW\n• MEDIUM\n• HIGH") + if content not in ['1', '2', '3']: + await mod_channel.send("Invalid option. Please choose:\n1. LOW\n2. MEDIUM\n3. HIGH") return - ctx['danger_level'] = content - if content == 'low': - await mod_channel.send("Flag post as low danger. After claim is investigated, what action should be taken on post?\n• DO NOT RECOMMEND\n• FLAG AS UNPROVEN") + danger_levels = {'1': 'low', '2': 'medium', '3': 'high'} + ctx['danger_level'] = danger_levels[content] + if content == '1': # LOW + await mod_channel.send("Flag post as low danger. After claim is investigated, what action should be taken on post?\n1. DO NOT RECOMMEND\n2. FLAG AS UNPROVEN") self.active_mod_flow['step'] = 'low_action_on_post' return - elif content == 'medium': - await mod_channel.send("Flag post as medium danger. After claim is investigated, what action should be taken on post?\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") + elif content == '2': # MEDIUM + await mod_channel.send("Flag post as medium danger. After claim is investigated, what action should be taken on post?\n1. REMOVE\n2. RAISE\n3. REPORT TO AUTHORITIES") self.active_mod_flow['step'] = 'medium_action_on_post' return - elif content == 'high': - await mod_channel.send("Flag post as high danger. What emergency action should be taken based on post?\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") + elif content == '3': # HIGH + await mod_channel.send("Flag post as high danger. What emergency action should be taken based on post?\n1. REMOVE\n2. RAISE\n3. REPORT TO AUTHORITIES") self.active_mod_flow['step'] = 'high_action_on_post' return if step == 'low_action_on_post': - if content == 'do not recommend': + if content not in ['1', '2']: + await mod_channel.send("Invalid option. Please choose:\n1. DO NOT RECOMMEND\n2. FLAG AS UNPROVEN") + return + if content == '1': # DO NOT RECOMMEND await mod_channel.send("Post will not be recommended. Action recorded. (Update algorithm so post is not recommended.)") await self.notify_reported_user(reported_user_name, guild, outcome="Post not recommended.") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Post not recommended" + ) self.active_mod_flow = None return - elif content == 'flag as unproven': + elif content == '2': # FLAG AS UNPROVEN await mod_channel.send("Post will be flagged as unproven/non-scientific. Please add explanation for why post is being flagged.") self.active_mod_flow['step'] = 'flag_explanation' return - else: - await mod_channel.send("Invalid option. Please choose:\n• DO NOT RECOMMEND\n• FLAG AS UNPROVEN") - return if step == 'flag_explanation': await mod_channel.send(f"Explanation recorded: {message.content}\nFlagged post as not proven.") await self.notify_reported_user(reported_user_name, guild, outcome="Post flagged as unproven/non-scientific.", explanation=message.content) + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Post flagged as unproven/non-scientific", + message.content + ) self.active_mod_flow = None return if step == 'medium_action_on_post' or step == 'high_action_on_post': - if content == 'remove': + if content not in ['1', '2', '3']: + await mod_channel.send("Invalid option. Please choose:\n1. REMOVE\n2. RAISE\n3. REPORT TO AUTHORITIES") + return + if content == '1': # REMOVE await mod_channel.send("Post will be removed. Please add explanation for why post is being removed.") self.active_mod_flow['step'] = 'remove_explanation' return - elif content == 'raise': + elif content == '2': # RAISE await mod_channel.send("Raising to higher level moderator. Report sent to higher level moderators.") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Report raised to higher level moderator" + ) self.active_mod_flow = None return - elif content == 'report to authorities': + elif content == '3': # REPORT TO AUTHORITIES await mod_channel.send("Reporting to authorities. Report sent to authorities.") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Reported to authorities" + ) self.active_mod_flow = None return - else: - await mod_channel.send("Invalid option. Please choose:\n• REMOVE\n• RAISE\n• REPORT TO AUTHORITIES") - return if step == 'remove_explanation': - await mod_channel.send(f"Explanation recorded: {message.content}\nPost removed. What action should be taken on the creator of the post?\n• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER") + await mod_channel.send(f"Explanation recorded: {message.content}\nPost removed. What action should be taken on the creator of the post?\n1. RECORD INCIDENT\n2. TEMPORARILY MUTE\n3. REMOVE USER") ctx['remove_explanation'] = message.content await self.notify_reported_user( reported_user_name, @@ -232,21 +267,42 @@ async def handle_mod_channel_message(self, message): self.active_mod_flow['step'] = 'action_on_user' return if step == 'action_on_user': - if content == 'record incident': + if content not in ['1', '2', '3']: + await mod_channel.send("Invalid option. Please choose:\n1. RECORD INCIDENT\n2. TEMPORARILY MUTE\n3. REMOVE USER") + return + if content == '1': # RECORD INCIDENT await mod_channel.send("Incident recorded for internal use. (Add to internal incident count for user.)") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Post removed and incident recorded", + ctx.get('remove_explanation', '') + ) self.active_mod_flow = None return - elif content == 'temporarily mute': + elif content == '2': # TEMPORARILY MUTE await mod_channel.send("User will be muted for 24 hours.") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Post removed and user temporarily muted", + ctx.get('remove_explanation', '') + ) self.active_mod_flow = None return - elif content == 'remove user': + elif content == '3': # REMOVE USER await mod_channel.send("User will be removed.") + self.user_stats.add_report( + reported_user.id, + report_type, + report_content, + "Post removed and user removed", + ctx.get('remove_explanation', '') + ) self.active_mod_flow = None return - else: - await mod_channel.send("Invalid option. Please choose:\n• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER") - return async def prompt_next_moderation_step(self, mod_channel): await mod_channel.send("Moderator, please review the report and respond with your decision.") diff --git a/DiscordBot/report.py b/DiscordBot/report.py index fb510ab0..a67bf932 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -99,18 +99,31 @@ async def handle_message(self, message): self.state = State.AWAITING_ABUSE_TYPE reply = "What type of abuse would you like to report?\n" - reply += "• BULLYING\n" - reply += "• SUICIDE/SELF-HARM\n" - reply += "• SEXUALLY EXPLICIT/NUDITY\n" - reply += "• MISINFORMATION\n" - reply += "• HATE SPEECH\n" - reply += "• DANGER" + reply += "1. BULLYING\n" + reply += "2. SUICIDE/SELF-HARM\n" + reply += "3. SEXUALLY EXPLICIT/NUDITY\n" + reply += "4. MISINFORMATION\n" + reply += "5. HATE SPEECH\n" + reply += "6. DANGER" return ["I found this message:", "```" + self.message.author.name + ": " + self.message.content + "```", reply] if self.state == State.AWAITING_ABUSE_TYPE: - abuse_type = message.content.lower() - if abuse_type in SUICIDE_VARIANTS: - self.abuse_type = AbuseType.SUICIDE + abuse_type = message.content.strip() + abuse_types = { + '1': AbuseType.BULLYING, + '2': AbuseType.SUICIDE, + '3': AbuseType.EXPLICIT, + '4': AbuseType.MISINFORMATION, + '5': AbuseType.HATE, + '6': AbuseType.DANGER + } + + if abuse_type not in abuse_types: + return ["Please select a valid option (1-6) from the list above."] + + self.abuse_type = abuse_types[abuse_type] + + if self.abuse_type == AbuseType.SUICIDE: mod_channel = self.client.mod_channels[self.message.guild.id] await mod_channel.send(f"SUICIDE/SELF-HARM REPORT:\n{self.message.author.name}: {self.message.content}") await self.client.start_moderation_flow( @@ -121,8 +134,7 @@ async def handle_message(self, message): self.state = State.REPORT_COMPLETE return ["Thank you for reporting. This has been sent to our moderation team for review."] - if abuse_type in EXPLICIT_VARIANTS: - self.abuse_type = AbuseType.EXPLICIT + if self.abuse_type == AbuseType.EXPLICIT: mod_channel = self.client.mod_channels[self.message.guild.id] await mod_channel.send(f"EXPLICIT CONTENT REPORT:\n{self.message.author.name}: {self.message.content}") await self.client.start_moderation_flow( @@ -133,77 +145,94 @@ async def handle_message(self, message): self.state = State.REPORT_COMPLETE return ["Thank you for reporting. This has been sent to our moderation team for review."] - for type in AbuseType: - if abuse_type == type.value: - self.abuse_type = type - if type == AbuseType.MISINFORMATION: - self.state = State.AWAITING_MISINFO_CATEGORY - return ["Please select the misinformation category:\n• HEALTH\n• ADVERTISEMENT\n• NEWS"] - else: - mod_channel = self.client.mod_channels[self.message.guild.id] - await mod_channel.send(f"New report - {type.value.upper()}:\n{self.message.author.name}: {self.message.content}") - await self.client.start_moderation_flow( - report_type=type.value.upper(), - report_content=self.message.content, - message_author=self.message.author.name - ) - self.state = State.REPORT_COMPLETE - return ["Thank you for reporting, it has been sent to our moderation team."] - return ["Please select a valid abuse type from the list above."] + if self.abuse_type == AbuseType.MISINFORMATION: + self.state = State.AWAITING_MISINFO_CATEGORY + return ["Please select the misinformation category:\n1. HEALTH\n2. ADVERTISEMENT\n3. NEWS"] + else: + mod_channel = self.client.mod_channels[self.message.guild.id] + await mod_channel.send(f"New report - {self.abuse_type.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=self.abuse_type.value.upper(), + report_content=self.message.content, + message_author=self.message.author.name + ) + self.state = State.REPORT_COMPLETE + return ["Thank you for reporting, it has been sent to our moderation team."] if self.state == State.AWAITING_MISINFO_CATEGORY: - category = message.content.lower() - for cat in MisinfoCategory: - if category == cat.value: - self.misinfo_category = cat - if cat == MisinfoCategory.HEALTH: - self.state = State.AWAITING_HEALTH_CATEGORY - return ["Please specify the health misinformation category:\n• EMERGENCY\n• MEDICAL RESEARCH\n• REPRODUCTIVE HEALTHCARE\n• TREATMENTS\n• ALTERNATIVE MEDICINE"] - elif cat == MisinfoCategory.NEWS: - self.state = State.AWAITING_NEWS_CATEGORY - return ["Please specify the news category:\n• HISTORICAL\n• POLITICAL\n• SCIENCE"] - else: # Advertisement - self.state = State.REPORT_COMPLETE - await self.client.mod_channels[self.message.guild.id].send(f"ADVERTISING MISINFO:\n{self.message.author.name}: {self.message.content}") - await self.client.start_moderation_flow( - report_type="ADVERTISING MISINFO", - report_content=self.message.content, - message_author=self.message.author.name - ) - return ["This has been reported to our ad team."] - return ["Please select a valid misinformation category from the list above."] + category = message.content.strip() + misinfo_categories = { + '1': MisinfoCategory.HEALTH, + '2': MisinfoCategory.ADVERTISEMENT, + '3': MisinfoCategory.NEWS + } + + if category not in misinfo_categories: + return ["Please select a valid option (1-3) from the list above."] + + self.misinfo_category = misinfo_categories[category] + + if self.misinfo_category == MisinfoCategory.HEALTH: + self.state = State.AWAITING_HEALTH_CATEGORY + return ["Please specify the health misinformation category:\n1. EMERGENCY\n2. MEDICAL RESEARCH\n3. REPRODUCTIVE HEALTHCARE\n4. TREATMENTS\n5. ALTERNATIVE MEDICINE"] + elif self.misinfo_category == MisinfoCategory.NEWS: + self.state = State.AWAITING_NEWS_CATEGORY + return ["Please specify the news category:\n1. HISTORICAL\n2. POLITICAL\n3. SCIENCE"] + else: # Advertisement + self.state = State.REPORT_COMPLETE + await self.client.mod_channels[self.message.guild.id].send(f"ADVERTISING MISINFO:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type="ADVERTISING MISINFO", + report_content=self.message.content, + message_author=self.message.author.name + ) + return ["This has been reported to our ad team."] if self.state == State.AWAITING_HEALTH_CATEGORY: - health_cat = message.content.lower() - for cat in HealthCategory: - if health_cat == cat.value: - self.specific_category = cat - self.state = State.REPORT_COMPLETE - mod_channel = self.client.mod_channels[self.message.guild.id] - await mod_channel.send(f"HEALTH MISINFO - {cat.value.upper()}:\n{self.message.author.name}: {self.message.content}") - await self.client.start_moderation_flow( - report_type=f"HEALTH MISINFO - {cat.value.upper()}", - report_content=self.message.content, - message_author=self.message.author.name - ) - return ["This has been sent to our moderation team."] - return ["Please select a valid health category from the list above."] + health_cat = message.content.strip() + health_categories = { + '1': HealthCategory.EMERGENCY, + '2': HealthCategory.MEDICAL_RESEARCH, + '3': HealthCategory.REPRODUCTIVE, + '4': HealthCategory.TREATMENTS, + '5': HealthCategory.ALTERNATIVE + } + + if health_cat not in health_categories: + return ["Please select a valid option (1-5) from the list above."] + + self.specific_category = health_categories[health_cat] + self.state = State.REPORT_COMPLETE + mod_channel = self.client.mod_channels[self.message.guild.id] + await mod_channel.send(f"HEALTH MISINFO - {self.specific_category.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=f"HEALTH MISINFO - {self.specific_category.value.upper()}", + report_content=self.message.content, + message_author=self.message.author.name + ) + return ["This has been sent to our moderation team."] if self.state == State.AWAITING_NEWS_CATEGORY: - news_cat = message.content.lower() - for cat in NewsCategory: - if news_cat == cat.value: - self.specific_category = cat - self.state = State.REPORT_COMPLETE - mod_channel = self.client.mod_channels[self.message.guild.id] - await mod_channel.send(f"NEWS MISINFO - {cat.value.upper()}:\n{self.message.author.name}: {self.message.content}") - await self.client.start_moderation_flow( - report_type=f"NEWS MISINFO - {cat.value.upper()}", - report_content=self.message.content, - message_author=self.message.author.name - ) - return ["This has been sent to our team."] - return ["Please select a valid news category from the list above."] + news_cat = message.content.strip() + news_categories = { + '1': NewsCategory.HISTORICAL, + '2': NewsCategory.POLITICAL, + '3': NewsCategory.SCIENCE + } + + if news_cat not in news_categories: + return ["Please select a valid option (1-3) from the list above."] + + self.specific_category = news_categories[news_cat] + self.state = State.REPORT_COMPLETE + mod_channel = self.client.mod_channels[self.message.guild.id] + await mod_channel.send(f"NEWS MISINFO - {self.specific_category.value.upper()}:\n{self.message.author.name}: {self.message.content}") + await self.client.start_moderation_flow( + report_type=f"NEWS MISINFO - {self.specific_category.value.upper()}", + report_content=self.message.content, + message_author=self.message.author.name + ) + return ["This has been sent to our team."] return [] diff --git a/DiscordBot/user_stats.json b/DiscordBot/user_stats.json new file mode 100644 index 00000000..3b250c4a --- /dev/null +++ b/DiscordBot/user_stats.json @@ -0,0 +1,21 @@ +{ + "1364364429797363722": { + "total_reports": 2, + "reports": [ + { + "timestamp": "2025-05-27T19:16:54.138877", + "report_type": "HEALTH MISINFO - TREATMENTS", + "report_content": "this is health misinformation", + "outcome": "Report raised to higher level moderator", + "explanation": null + }, + { + "timestamp": "2025-05-27T19:17:33.416435", + "report_type": "HEALTH MISINFO - TREATMENTS", + "report_content": "this is health misinformation", + "outcome": "Post removed and user temporarily muted", + "explanation": "not true" + } + ] + } +} \ No newline at end of file diff --git a/DiscordBot/user_stats.py b/DiscordBot/user_stats.py new file mode 100644 index 00000000..cc2165f7 --- /dev/null +++ b/DiscordBot/user_stats.py @@ -0,0 +1,53 @@ +import json +import os +from datetime import datetime + +class UserStats: + def __init__(self): + self.stats_file = 'user_stats.json' + # Clear the stats file on initialization + self._clear_stats() + self.stats = self._load_stats() + + def _clear_stats(self): + # Create an empty stats file + with open(self.stats_file, 'w') as f: + json.dump({}, f) + + def _load_stats(self): + if os.path.exists(self.stats_file): + with open(self.stats_file, 'r') as f: + return json.load(f) + return {} + + def _save_stats(self): + with open(self.stats_file, 'w') as f: + json.dump(self.stats, f, indent=2) + + def add_report(self, user_id, report_type, report_content, outcome, explanation=None): + if user_id not in self.stats: + self.stats[user_id] = { + 'total_reports': 0, + 'reports': [] + } + + report = { + 'timestamp': datetime.now().isoformat(), + 'report_type': report_type, + 'report_content': report_content, + 'outcome': outcome, + 'explanation': explanation + } + + self.stats[user_id]['reports'].append(report) + self.stats[user_id]['total_reports'] = len(self.stats[user_id]['reports']) + self._save_stats() + + def get_user_stats(self, user_id): + return self.stats.get(user_id, { + 'total_reports': 0, + 'reports': [] + }) + + def get_all_stats(self): + return self.stats \ No newline at end of file From fb8f305ec3f58df7dd77d1e52bfda6cc04374d93 Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Tue, 27 May 2025 19:30:30 -0700 Subject: [PATCH 5/7] stashing changes --- DiscordBot/report.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/DiscordBot/report.py b/DiscordBot/report.py index 45480a1f..72ce422b 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -22,25 +22,6 @@ class AbuseType(Enum): HATE = "hate speech" DANGER = "danger" -SUICIDE_VARIANTS = { - "suicide", - "self harm", - "self-harm", - "selfharm", - "suicide/self harm", - "suicide/selfharm", - "suicide/self-harm", -} - -EXPLICIT_VARIANTS = { - "explicit", - "sexually explicit", - "sexual", - "nudity", - "nude", - "sexually explicit/nudity", -} - class MisinfoCategory(Enum): HEALTH = "health" ADVERTISEMENT = "advertisement" From 07e4031ffea5c5214bf11b30bd28485dbde25aef Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Tue, 27 May 2025 19:44:23 -0700 Subject: [PATCH 6/7] fixed merge errors --- DiscordBot/bot.py | 7 ++++--- DiscordBot/user_stats.json | 13 +++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index 1eb64035..d0b73086 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -111,7 +111,7 @@ async def handle_dm(self, message): 'context': {}, 'guild_id': info['guild_id'] } - await mod_chan.send("Moderators, please respond with:\n• ACCEPT\n• UPHOLD") + await mod_chan.send("Moderators, please respond with:\n1. ACCEPT\n2. UPHOLD") # Acknowledge to user await message.channel.send("Your appeal has been submitted and is under review.") @@ -236,7 +236,7 @@ async def handle_mod_channel_message(self, message): return else: - await mod_channel.send("Invalid response. Please respond with:\n• ACCEPT\n• UPHOLD") + await mod_channel.send("Invalid response. Please respond with:\n1. ACCEPT\n2. UPHOLD") return ctx = self.active_mod_flow['context'] @@ -358,7 +358,7 @@ async def handle_mod_channel_message(self, message): await mod_channel.send( f"Explanation recorded: {explanation}\n" "What action should be taken on the creator of the post?\n" - "• RECORD INCIDENT\n• TEMPORARILY MUTE\n• REMOVE USER" + "1. RECORD INCIDENT\n2. TEMPORARILY MUTE\n3. REMOVE USER" ) self.active_mod_flow['step'] = 'action_on_user' return @@ -385,6 +385,7 @@ async def handle_mod_channel_message(self, message): report_content, "Post removed and user temporarily muted", ctx.get('remove_explanation', '') + ) await self.notify_reported_user( reported_user_name, guild, diff --git a/DiscordBot/user_stats.json b/DiscordBot/user_stats.json index 3b250c4a..139db9d9 100644 --- a/DiscordBot/user_stats.json +++ b/DiscordBot/user_stats.json @@ -1,19 +1,12 @@ { "1364364429797363722": { - "total_reports": 2, + "total_reports": 1, "reports": [ { - "timestamp": "2025-05-27T19:16:54.138877", + "timestamp": "2025-05-27T19:39:16.228235", "report_type": "HEALTH MISINFO - TREATMENTS", "report_content": "this is health misinformation", - "outcome": "Report raised to higher level moderator", - "explanation": null - }, - { - "timestamp": "2025-05-27T19:17:33.416435", - "report_type": "HEALTH MISINFO - TREATMENTS", - "report_content": "this is health misinformation", - "outcome": "Post removed and user temporarily muted", + "outcome": "Post removed and incident recorded", "explanation": "not true" } ] From 256866dab69ce928feeff8a3ddab997cb3ae0492 Mon Sep 17 00:00:00 2001 From: michaelsouliman Date: Tue, 27 May 2025 19:48:46 -0700 Subject: [PATCH 7/7] fixed input for uphold/accept --- DiscordBot/bot.py | 4 ++-- DiscordBot/user_stats.json | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index d0b73086..5097a3de 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -219,7 +219,7 @@ async def handle_mod_channel_message(self, message): guild = mod_channel.guild if hasattr(mod_channel, 'guild') else None if step == 'appeal_review': - if content == 'accept': + if content == '1': await mod_channel.send("The appeal has been accepted. The original decision has been overturned.") user = discord.utils.get(guild.members, name=self.active_mod_flow['message_author']) if user: @@ -227,7 +227,7 @@ async def handle_mod_channel_message(self, message): self.active_mod_flow = None return - elif content == 'uphold': + elif content == '2': await mod_channel.send("The appeal has been reviewed and the original decision is upheld.") user = discord.utils.get(guild.members, name=self.active_mod_flow['message_author']) if user: diff --git a/DiscordBot/user_stats.json b/DiscordBot/user_stats.json index 139db9d9..8b4ae325 100644 --- a/DiscordBot/user_stats.json +++ b/DiscordBot/user_stats.json @@ -3,10 +3,22 @@ "total_reports": 1, "reports": [ { - "timestamp": "2025-05-27T19:39:16.228235", + "timestamp": "2025-05-27T19:46:25.232435", "report_type": "HEALTH MISINFO - TREATMENTS", "report_content": "this is health misinformation", - "outcome": "Post removed and incident recorded", + "outcome": "Post removed and user removed", + "explanation": "not true" + } + ] + }, + "484531188581793803": { + "total_reports": 1, + "reports": [ + { + "timestamp": "2025-05-27T19:47:11.110317", + "report_type": "HEALTH MISINFO - TREATMENTS", + "report_content": "this is news misinfo political", + "outcome": "Post removed and user removed", "explanation": "not true" } ]