diff --git a/DiscordBot/bot.py b/DiscordBot/bot.py index ec5dddb6..5097a3de 100644 --- a/DiscordBot/bot.py +++ b/DiscordBot/bot.py @@ -6,7 +6,8 @@ import logging import re import requests -from report import Report +from report import Report, AbuseType, MisinfoCategory, HealthCategory, NewsCategory, State +from user_stats import UserStats import pdb # Set up logging to the console @@ -30,10 +31,14 @@ 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 + 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:') @@ -71,6 +76,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:\n1. ACCEPT\n2. 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" @@ -89,7 +135,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) @@ -99,33 +145,289 @@ 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 - # 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+ "'" + 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?\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 + 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, 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"\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'] + 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 == '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: + await user.send("Your appeal has been accepted. The original decision has been overturned.") + self.active_mod_flow = None + return + + 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: + 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:\n1. ACCEPT\n2. 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'] + + # 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 ['1', '2', '3']: + await mod_channel.send("Invalid option. Please choose:\n1. LOW\n2. MEDIUM\n3. HIGH") + return + 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 == '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 == '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 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 == '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 + 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 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 == '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 == '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 + if step == 'remove_explanation': + explanation = message.content + ctx['remove_explanation'] = explanation + await self.notify_reported_user( + reported_user_name, + guild, + outcome="Post removed.", + 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" + "1. RECORD INCIDENT\n2. TEMPORARILY MUTE\n3. REMOVE USER" + ) + self.active_mod_flow['step'] = 'action_on_user' + return + if step == 'action_on_user': + 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 == '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', '') + ) + 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 == '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', '') + ) + 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 + 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 d2bba994..72ce422b 100644 --- a/DiscordBot/report.py +++ b/DiscordBot/report.py @@ -6,7 +6,38 @@ 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() + AWAITING_APPEAL = auto() + APPEAL_REVIEW = auto() + +class AbuseType(Enum): + BULLYING = "bullying" + SUICIDE = "suicide/self-harm" + EXPLICIT = "sexually explicit/nudity" + MISINFORMATION = "misinformation" + HATE = "hate speech" + DANGER = "danger" + +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" @@ -17,28 +48,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 +76,164 @@ 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 += "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] - # 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.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( + 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 sent to our moderation team for review."] - return [] + 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( + 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 sent to our moderation team for review."] - def report_complete(self): - return self.state == State.REPORT_COMPLETE - + 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.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.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.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 [] + + 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 diff --git a/DiscordBot/user_stats.json b/DiscordBot/user_stats.json new file mode 100644 index 00000000..8b4ae325 --- /dev/null +++ b/DiscordBot/user_stats.json @@ -0,0 +1,26 @@ +{ + "1364364429797363722": { + "total_reports": 1, + "reports": [ + { + "timestamp": "2025-05-27T19:46:25.232435", + "report_type": "HEALTH MISINFO - TREATMENTS", + "report_content": "this is health misinformation", + "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" + } + ] + } +} \ 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