diff --git a/BotCommands.py b/BotCommands.py index 09cb50c..39e9f89 100644 --- a/BotCommands.py +++ b/BotCommands.py @@ -11,6 +11,9 @@ def GetInstance(self): def IsActivated(self, InteractionId:int) -> bool: return (self.GetInstance().Database.IsActivatedInServer(InteractionId)) + + def CanReport(self, InteractionId:int) -> bool: + return (self.GetInstance().Database.CanServerReport(InteractionId)) @app_commands.command(name="check", description="Checks to see if a discord id is banned") @app_commands.checks.has_permissions(ban_members=True) @@ -26,7 +29,7 @@ async def ScamCheck_Global(self, interaction:Interaction, target:app_commands.Tr else: await interaction.response.send_message("Your server must be activated in order to run scam check!") - @app_commands.command(name="report", description="Report an User ID") + @app_commands.command(name="report", description="Report an User") @app_commands.checks.has_permissions(ban_members=True) @app_commands.checks.cooldown(1, 5.0) async def ReportScam_Global(self, interaction:Interaction, target:app_commands.Transform[int, TargetIdTransformer]): @@ -39,32 +42,31 @@ async def ReportScam_Global(self, interaction:Interaction, target:app_commands.T await interaction.response.send_message("You must activate your server to report users", ephemeral=True, delete_after=10.0) return + # Check if the server is barred from reporting + if (not self.CanReport(interaction.guild_id)): + await interaction.response.send_message("Due to potential abuse, this command is limited in this server, please contact support.", ephemeral=True, delete_after=10.0) + return + + # If it cannot be transformed, print an error + if (target == -1): + await interaction.response.send_message("Could not look up the given user, supported look up methods are via @ mentions and user ids", ephemeral=True) + return + UserToSend:Member|User|None = await self.GetInstance().LookupUser(target, ServerToInspect=interaction.guild) # If the user is no longer in said server, then do a global lookup if (UserToSend is None): UserToSend = await self.GetInstance().LookupUser(target) - + # If the user is still invalid, then ask for a manual report. if (UserToSend is None): await interaction.response.send_message("Unable to look up the given user for a report, you'll have to make a report manually.", ephemeral=True) return - await interaction.response.send_modal(SubmitScamReport(UserToSend)) - - @app_commands.command(name="reportuser", description="Report a user by a mention selector") - @app_commands.checks.has_permissions(ban_members=True) - @app_commands.checks.cooldown(1, 5.0) - async def ReportScamUser_Global(self, interaction:Interaction, user:Member): - if (interaction.guild_id == Config()["ControlServer"]): - await interaction.response.send_message("This command cannot be used in the control server", ephemeral=True, delete_after=5.0) - return - - # Block any usages of the commands if the server is not activated. - if (not self.IsActivated(interaction.guild_id)): - await interaction.response.send_message("You must activate your server to report users", ephemeral=True, delete_after=10.0) - return + # Check if the user is already banned + if (interaction.client.Database.DoesBanExist(UserToSend.id)): + await interaction.response.send_message(f"The targeted user is already banned by ScamGuard.", ephemeral=True, delete_after=20.0) - await interaction.response.send_modal(SubmitScamReport(user)) + await interaction.response.send_modal(SubmitScamReport(UserToSend)) @app_commands.command(name="setup", description="Set up ScamGuard") @app_commands.checks.has_permissions(ban_members=True) @@ -78,7 +80,7 @@ async def SetupScamGuard_Global(self, interaction:Interaction): if (not self.IsActivated(interaction.guild_id)): await self.GetInstance().ServerSetupHelper.OpenServerSetupModel(interaction) else: - await interaction.response.send_message("This server is already activated with ScamGuard!", ephemeral=True, delete_after=15.0) + await interaction.response.send_message("This server is already activated with ScamGuard! Use `/scamguard config` to change settings", ephemeral=True, delete_after=15.0) @app_commands.command(name="config", description="Set ScamGuard Settings") diff --git a/BotDatabase.py b/BotDatabase.py index 84e9ad8..d2fd5c1 100644 --- a/BotDatabase.py +++ b/BotDatabase.py @@ -142,6 +142,26 @@ def RemoveServerEntry(self, ServerId:int, BotId:int): self.Database.delete(server) self.Database.commit() + + def ToggleServerBan(self, ServerId:int, NewStatus:bool): + stmt = select(Server).where(Server.discord_server_id==ServerId) + server = self.Database.scalars(stmt).first() + if (server is None): + return + + server.should_ban_in = NewStatus + self.Database.add(server) + self.Database.commit() + + def ToggleServerReport(self, ServerId:int, NewStatus:bool): + stmt = select(Server).where(Server.discord_server_id==ServerId) + server = self.Database.scalars(stmt).first() + if (server is None): + return + + server.can_report = NewStatus + self.Database.add(server) + self.Database.commit() def SetBotActivationForOwner(self, Servers:list[int], IsActive:bool, BotId:int, OwnerId:int=-1, ActivatorId:int=-1): NumActivationChanges = 0 @@ -254,6 +274,18 @@ def IsActivatedInServer(self, ServerId:int) -> bool: return True return False + + def CanServerReport(self, ServerId:int) -> bool: + if (not self.IsInServer(ServerId)): + return False + + stmt = select(Server).where(Server.discord_server_id==ServerId) + server = self.Database.scalars(stmt).first() + + if (server.can_report): + return True + + return False def DoesBanExist(self, TargetId:int) -> bool: stmt = select(Ban).where(Ban.discord_user_id==TargetId) @@ -275,7 +307,7 @@ def GetServerInfo(self, ServerId:int) -> Server: return self.Database.scalars(stmt).first() ### Adding/Removing Bans ### - def AddBan(self, TargetId:int, BannerName:str, BannerId:int) -> BanLookup: + def AddBan(self, TargetId:int, BannerName:str, BannerId:int, ThreadId:int|None) -> BanLookup: if (self.DoesBanExist(TargetId)): return BanLookup.Duplicate @@ -284,6 +316,9 @@ def AddBan(self, TargetId:int, BannerName:str, BannerId:int) -> BanLookup: assigner_discord_user_id = BannerId, assigner_discord_user_name = BannerName ) + + if (ThreadId is not None): + ban.evidence_thread = ThreadId self.Database.add(ban) self.Database.commit() @@ -302,6 +337,22 @@ def RemoveBan(self, TargetId:int) -> BanLookup: return BanLookup.Good + ### Updating Ban Data ### + def SetEvidenceThread(self, TargetId:int, ThreadId:int): + if (TargetId <= 0 or ThreadId <= 0): + return + + if (not self.DoesBanExist(TargetId)): + return + + stmt = select(Ban).where(Ban.id==TargetId) + if (stmt is None): + return + + banToChange = self.Database.scalars(stmt).first() + banToChange.evidence_thread = ThreadId + self.Database.add(banToChange) + ### Getting Server Information ### def GetAllServersOfOwner(self, OwnerId:int) -> list[Server]: stmt = select(Server).where(Server.owner_discord_user_id==OwnerId) @@ -354,7 +405,7 @@ def GetAllBans(self, NumLastActions:int=0) -> list[Ban]: return list(self.Database.scalars(stmt).all()) - def GetAllServers(self, ActivationState:bool=False, OfInstance:int=-1) -> list[Server]: + def GetAllServers(self, ActivationState:bool=False, OfInstance:int=-1, FilterBanability:bool=False) -> list[Server]: stmt = select(Server) if (ActivationState): @@ -362,12 +413,18 @@ def GetAllServers(self, ActivationState:bool=False, OfInstance:int=-1) -> list[S if (OfInstance > -1): stmt = stmt.where(Server.bot_instance_id==OfInstance) + + if (FilterBanability): + stmt = stmt.where(Server.should_ban_in==1) return list(self.Database.scalars(stmt).all()) def GetAllActivatedServers(self, OfInstance:int=-1) -> list[Server]: return self.GetAllServers(True, OfInstance) + def GetAllActivatedServersWithBans(self, OfInstance:int=-1) -> list[Server]: + return self.GetAllServers(True, OfInstance, True) + def GetAllDeactivatedServers(self) -> list[Server]: stmt = select(Server).where(Server.activation_state==False) diff --git a/BotDatabaseSchema.py b/BotDatabaseSchema.py index 5a5947b..b94e7cf 100644 --- a/BotDatabaseSchema.py +++ b/BotDatabaseSchema.py @@ -1,5 +1,5 @@ from sqlalchemy import Column, Integer, DateTime, String -from sqlalchemy.sql import func +from sqlalchemy.sql import func, null from sqlalchemy.orm import declarative_base Base = declarative_base() @@ -21,6 +21,7 @@ class Ban(Base): assigner_discord_user_name = Column(String(32), nullable=False) created_at = Column(DateTime(), server_default=func.now()) updated_at = Column(DateTime(), server_default=func.now(), onupdate=func.now()) + evidence_thread = Column(Integer, nullable=True, server_default=null()) class Server(Base): __tablename__ = "servers" @@ -36,3 +37,5 @@ class Server(Base): message_channel = Column(Integer, server_default="0") has_webhooks = Column(Integer, server_default="0") kick_sus_users = Column(Integer, server_default="0") + can_report = Column(Integer, server_default="1") + should_ban_in = Column(Integer, server_default="1") diff --git a/BotEnums.py b/BotEnums.py index d328522..5ae378c 100644 --- a/BotEnums.py +++ b/BotEnums.py @@ -14,6 +14,7 @@ class BanResult(CompareEnum): NotBanned=auto() InvalidUser=auto() LostPermissions=auto() + BansExceeded=auto() ServerOwner=auto() Error=auto() @@ -33,4 +34,5 @@ class RelayMessageType(CompareEnum): # TODO: In future to remove the number of writers AddedToServer=auto() RemovedFromServer=auto() - ServerOwnerChanged=auto() \ No newline at end of file + ServerOwnerChanged=auto() + # TODO: Add enqueue messages to fix issue #64 \ No newline at end of file diff --git a/BotMain.py b/BotMain.py index 315beb1..a945fc0 100644 --- a/BotMain.py +++ b/BotMain.py @@ -410,7 +410,7 @@ async def PostScamReport(self, ReportData): ### Webhook Management ### async def InstallWebhook(self, ServerId:int): ChannelID:int = self.Database.GetChannelIdForServer(ServerId) - MessageChannel:discord.TextChannel = self.get_channel(ChannelID) + MessageChannel:discord.TextChannel = self.get_channel(ChannelID) # Check to see if a webhook is already installed. if (MessageChannel is not None): @@ -467,6 +467,9 @@ async def DeleteWebhook(self, ServerId:int): def GetServerInfoStr(self, Server:discord.Guild) -> str: return f"{Server.name}[{Server.id}]" + def GetControlServerGuild(self) -> discord.Guild: + return self.get_guild(ConfigData["ControlServer"]) + def PostPongMessage(self): Logger.Log(LogLevel.Notice, "I have been pinged!") @@ -515,6 +518,9 @@ async def CreateBanEmbed(self, TargetId:int) -> discord.Embed: if (HasUserData): UserData.add_field(name="Name", value=User.display_name) UserData.add_field(name="Handle", value=User.mention) + # If currently banned and has an evidence thread, display it. + if (UserBanned and BanData.evidence_thread is not None): + UserData.add_field(name="Evidence Thread (TAG Server)", value=f"<#{BanData.evidence_thread}>") # This will always be an approximation, plus they may be in servers the bot is not in. if (ConfigData["ScamCheckShowsSharedServers"]): UserData.add_field(name="Shared Servers", value=f"~{len(User.mutual_guilds)}") @@ -572,6 +578,10 @@ async def ReprocessBans(self, ServerId:int, LastActions:int=0) -> BanResult: Logger.Log(LogLevel.Error, f"Unable to process ban on user {UserId} for server {ServerInfoStr}") BanReturn = BanResult.LostPermissions break + elif (BanResponseFlag == BanResult.BansExceeded): + Logger.Log(LogLevel.Error, f"Unable to process ban on user {UserId} for server {ServerInfoStr} due to exceed") + BanReturn = BanResult.BansExceeded + break else: NumBans += 1 Logger.Log(LogLevel.Notice, f"Processed {NumBans}/{TotalBans} bans for {ServerInfoStr}!") @@ -608,7 +618,7 @@ async def ProcessActionOnUser(self, TargetId:int, AuthorizerName:str, IsBan:bool ScamStr = "non-scammer" BanReason=f"Confirmed {ScamStr} by {AuthorizerName}" - AllServers = self.Database.GetAllActivatedServers(self.BotID) + AllServers = self.Database.GetAllActivatedServersWithBans(self.BotID) NumServers:int = len(AllServers) # Instead of going through all servers it's added to, choose all servers that are activated. @@ -636,7 +646,7 @@ async def ProcessActionOnUser(self, TargetId:int, AuthorizerName:str, IsBan:bool elif (ResultFlag == BanResult.ServerOwner): Logger.Log(LogLevel.Error, f"Attempted to ban a server owner! {self.GetServerInfoStr(DiscordServer)} with user to work {UserToWorkOn.id} == {DiscordServer.owner_id}") continue - elif (ResultFlag == BanResult.LostPermissions or ResultFlag == BanResult.Error): + elif (ResultFlag == BanResult.LostPermissions or ResultFlag == BanResult.Error or ResultFlag == BanResult.BansExceeded): self.AddAsyncTask(self.PostBanFailureInformation(DiscordServer, TargetId, ResultFlag, IsBan)) else: # TODO: Potentially remove the server from the list? @@ -678,6 +688,10 @@ async def PerformActionOnServer(self, Server:discord.Guild, User:discord.Member, Logger.Log(LogLevel.Error, f"We do not have ban/unban permissions in this server {ServerInfo} owned by {ServerOwnerId}! Err: {str(forbiddenEx)}") return (False, BanResult.LostPermissions) except discord.HTTPException as ex: + if (ex.code == 30035): + Logger.Log(LogLevel.Warn, f"Hit the bans exceeded error while trying to perform actions on server {ServerInfo}") + return (False, BanResult.BansExceeded) + Logger.Log(LogLevel.Warn, f"We encountered an error {(str(ex))} while trying to perform for server {ServerInfo} owned by {ServerOwnerId}!") return (False, BanResult.Error) @@ -701,7 +715,7 @@ async def PostBanFailureInformation(self, Server:discord.Guild, UserId:int, Reas if (Reason == BanResult.LostPermissions): ErrorMsg = "ScamGuard does not have significant permissions to ban this user" ResolutionMsg = "This usually happens if the user in question has grabbed roles that are higher than the bot's.\n\nYou can usually fix this by changing the order as seen in [this video](https://youtu.be/XYaQi3hM9ug), or giving ScamGuard a moderation role." - elif (Reason == BanResult.Error): + elif (Reason == BanResult.Error or Reason == BanResult.BansExceeded): ErrorMsg = "ScamGuard encountered an unknown error" ResolutionMsg = "This can happen when the Discord API has a hiccup, a ban will retry again soon." else: diff --git a/BotSetup.py b/BotSetup.py index 925ee5c..d07ebca 100644 --- a/BotSetup.py +++ b/BotSetup.py @@ -7,7 +7,7 @@ from BotDatabaseSchema import Base, Migration, Ban, Server class DatabaseMigrator: - DATABASE_VERSION=4 + DATABASE_VERSION=5 VersionMap={} DatabaseCon=None @@ -126,6 +126,14 @@ def upgrade_version3to4(self) -> bool: session.execute(text("ALTER TABLE servers ADD kick_sus_users INTEGER default 0")) session.commit() return True + + def upgrade_version4to5(self) -> bool: + session = Session(self.DatabaseCon) + session.execute(text("ALTER TABLE bans ADD evidence_thread INTEGER default NULL")) + session.execute(text("ALTER TABLE servers ADD can_report INTEGER default 1")) + session.execute(text("ALTER TABLE servers ADD should_ban_in INTEGER default 1")) + session.commit() + return True def SetupDatabases(): Logger.Log(LogLevel.Notice, "Loading database for scam bot setup") diff --git a/ConfirmBanView.py b/ConfirmBanView.py index 24c4b87..2dff99b 100644 --- a/ConfirmBanView.py +++ b/ConfirmBanView.py @@ -9,7 +9,7 @@ class ConfirmBan(SelfDeletingView): Hook:WebhookMessage = None def __init__(self, target:int, bot): - super().__init__(ViewTimeout=60.0) + super().__init__(ViewTimeout=90.0) self.TargetId = target self.ScamBot = bot @@ -30,7 +30,7 @@ async def confirm(self, interaction: Interaction, button: ui.Button): await interaction.response.defer(thinking=True) self.HasInteracted = True - Result:BanLookup = await self.ScamBot.HandleBanAction(self.TargetId, Sender, True) + Result:BanLookup = await self.ScamBot.HandleBanAction(self.TargetId, Sender, True, interaction.channel_id) if (Result is not BanLookup.Banned): if (Result == BanLookup.Duplicate): ResponseMsg = f"{self.TargetId} already exists in the ban database" diff --git a/Main.py b/Main.py index b9d34c9..fcf4681 100644 --- a/Main.py +++ b/Main.py @@ -203,6 +203,54 @@ async def ScamCheck_Control(interaction:Interaction, target:app_commands.Transfo ResponseEmbed:Embed = await ScamGuardBot.CreateBanEmbed(target) await interaction.response.send_message(embed = ResponseEmbed) + + # Control Server command to set evidence threads + @ScamGuardBot.Commands.command(name="setthread", description="In the control server, set the evidence thread for the given user id", guild=CommandControlServer) + @app_commands.checks.has_role(ConfigData["ApproverRole"]) + async def SetThread_Control(interaction:Interaction, target:app_commands.Transform[int, TargetIdTransformer]): + if (target <= -1): + await interaction.response.send_message("Invalid id!", ephemeral=True, delete_after=5.0) + return + + if (not ScamGuardBot.Database.DoesBanExist(target)): + await interaction.response.send_message("Cannot set an evidence thread on a non-ban at this time!", ephemeral=True) + return + + ScamGuardBot.Database.SetEvidenceThread(target, interaction.channel_id) + await interaction.response.send_message(f"Updated the thread for {target} to <#{interaction.channel_id}>") + Logger.Log(LogLevel.Log, f"Thread set for {target} to {interaction.channel_id}") + + # Togglers for curbing any potential abuse + @ScamGuardBot.Commands.command(name="toggleserverban", description="In the control server, sets if the given server should have bans processed on them", guild=CommandControlServer) + @app_commands.checks.has_role(ConfigData["MaintainerRole"]) + async def SetBanActionForServer_Control(interaction:Interaction, server:app_commands.Transform[int, ServerIdTransformer], state:bool): + if (server <= -1): + await interaction.response.send_message("Invalid id!", ephemeral=True, delete_after=5.0) + return + + if (not ScamGuardBot.Database.IsInServer(server)): + await interaction.response.send_message(f"ScamGuard is not in server {server}!", ephemeral=True, delete_after=5.0) + return + + ScamGuardBot.Database.ToggleServerBan(server, state) + await interaction.response.send_message(f"Server {server} ban ability set to {state}", ephemeral=True, delete_after=10.0) + Logger.Log(LogLevel.Log, f"Ban ability set for {server} to {state}") + + @ScamGuardBot.Commands.command(name="toggleserverreport", description="In the control server, sets if the given server should have bans processed on them", guild=CommandControlServer) + @app_commands.checks.has_role(ConfigData["MaintainerRole"]) + async def SetBanActionForServer_Control(interaction:Interaction, server:app_commands.Transform[int, ServerIdTransformer], state:bool): + if (server <= -1): + await interaction.response.send_message("Invalid id!", ephemeral=True, delete_after=5.0) + return + + if (not ScamGuardBot.Database.IsInServer(server)): + await interaction.response.send_message(f"ScamGuard is not in server {server}!", ephemeral=True, delete_after=5.0) + return + + ScamGuardBot.Database.ToggleServerReport(server, state) + await interaction.response.send_message(f"Server {server} report ability set to {state}", ephemeral=True, delete_after=10.0) + Logger.Log(LogLevel.Log, f"Report ability set for {server} to {state}") + SetupDatabases() ScamGuardBot.run(ConfigData.GetToken()) \ No newline at end of file diff --git a/ScamGuard.py b/ScamGuard.py index d49eaed..e355208 100644 --- a/ScamGuard.py +++ b/ScamGuard.py @@ -142,11 +142,11 @@ async def PublishAnnouncement(self, Message:str|discord.Embed): Logger.Log(LogLevel.Log, f"WARN: Unable to publish message to announcement channel {str(ex)}") ### Ban Handling ### - async def HandleBanAction(self, TargetId:int, Sender:discord.Member, PerformBan:bool) -> BanLookup: + async def HandleBanAction(self, TargetId:int, Sender:discord.Member, PerformBan:bool, ThreadId:int|None=None) -> BanLookup: DatabaseAction:BanLookup = None ActionTaken:str = "Ban" if PerformBan else "Unban" if (PerformBan): - DatabaseAction = self.Database.AddBan(TargetId, Sender.name, Sender.id) + DatabaseAction = self.Database.AddBan(TargetId, Sender.name, Sender.id, ThreadId) else: DatabaseAction = self.Database.RemoveBan(TargetId)