Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 328 additions & 26 deletions DiscordBot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:')
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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)
Loading