added reputation filter

Signed-off-by: Marc Ahlgrim <marc@onemarcfifty.com>
This commit is contained in:
Marc Ahlgrim
2022-09-29 21:43:04 +02:00
parent 6444ee2ce7
commit de648ef07c
2 changed files with 241 additions and 4 deletions
+57 -3
View File
@@ -14,6 +14,7 @@ from classes.dis_events import DiscordEvents
from classes.subscribe import Subscribe from classes.subscribe import Subscribe
from classes.subscribemenu import SubscribeView from classes.subscribemenu import SubscribeView
from classes.config import Config from classes.config import Config
from classes.reputation import Reputation
from dataclasses import dataclass from dataclasses import dataclass
@@ -25,6 +26,7 @@ from dataclasses import dataclass
class OMFBot(AutoShardedBot): class OMFBot(AutoShardedBot):
# each guild has the following elements: # each guild has the following elements:
# - a list of scheduled Events (EventsList) # - a list of scheduled Events (EventsList)
# - a list of Messages that will be sent at idle times # - a list of Messages that will be sent at idle times
@@ -36,6 +38,7 @@ class OMFBot(AutoShardedBot):
EventsList = {} EventsList = {}
idle_messages=[] idle_messages=[]
channel_idle_timer=0 channel_idle_timer=0
ReputationFilter:Reputation = None
# the guildDataList contains one GuildData class per item. # the guildDataList contains one GuildData class per item.
# the key is the guild ID # the key is the guild ID
@@ -63,6 +66,7 @@ class OMFBot(AutoShardedBot):
self.prefix="!" self.prefix="!"
self.configData=Config('config.json') self.configData=Config('config.json')
# The subscribe command will add/remove the notification roles # The subscribe command will add/remove the notification roles
# based on the scheduled events # based on the scheduled events
@self.tree.command(name="subscribe", description="(un)subscribe to Events)") @self.tree.command(name="subscribe", description="(un)subscribe to Events)")
@@ -123,8 +127,9 @@ class OMFBot(AutoShardedBot):
guildNode=self.configData.readGuild(interaction.guild.id) guildNode=self.configData.readGuild(interaction.guild.id)
eventNodes=guildNode["AUTO_EVENTS"] eventNodes=guildNode["AUTO_EVENTS"]
numEvents=len(eventNodes) numEvents=len(eventNodes)
numRoles=len(self.guildDataList[f'{interaction.guild.id}'].ReputationFilter.reputationRoles)
await interaction.response.send_message(f'{numEvents} Events and {numMessages} Message templates\nThank you for using my services!', ephemeral=True) await interaction.response.send_message(f'{numRoles} reputation roles, {numEvents} Events and {numMessages} Message templates\nThank you for using my services!', ephemeral=True)
except Exception as e: except Exception as e:
print(f"ERROR in update command: {e}") print(f"ERROR in update command: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True) await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
@@ -145,6 +150,16 @@ class OMFBot(AutoShardedBot):
print(f"ERROR in say_ralf: {e}") print(f"ERROR in say_ralf: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True) await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
@self.tree.command(name="reputation", description="shows your reputation")
async def update(interaction: discord.Interaction):
try:
userReputation=self.guildDataList[f'{interaction.guild.id}'].ReputationFilter.getReputation(interaction.user,True)
await interaction.response.send_message(userReputation, ephemeral=True)
except Exception as e:
print(f"ERROR in update command: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
# ################################ # ################################
# the bot run command just starts # the bot run command just starts
@@ -215,27 +230,58 @@ class OMFBot(AutoShardedBot):
async def readMessageTemplates(self,theGuild:discord.Guild): async def readMessageTemplates(self,theGuild:discord.Guild):
# we init the guild data with a new GuildData object # we init the guild data with a new GuildData object
self.guildDataList[f'{theGuild.id}'] = self.GuildData() self.guildDataList[f'{theGuild.id}'] = self.GuildData(None)
guildNode=self.configData.readGuild(theGuild.id) guildNode=self.configData.readGuild(theGuild.id)
if guildNode is None: if guildNode is None:
print (f"Guild {theGuild.id} has no setup") print (f"Guild {theGuild.id} has no setup")
return return
# if we have data for that guild then we try to read in the template messages from
# the predefined template channel
theTemplateChannel:discord.TextChannel theTemplateChannel:discord.TextChannel
theTemplateChannel=theGuild.get_channel(int(guildNode["CONFIG_CHANNEL_ID"])) theTemplateChannel=theGuild.get_channel(int(guildNode["CONFIG_CHANNEL_ID"]))
message:discord.Message message:discord.Message
messages = theTemplateChannel.history(limit=50) messages = theTemplateChannel.history(limit=50)
self.guildDataList[f'{theGuild.id}'].idle_messages=[] self.guildDataList[f'{theGuild.id}'].idle_messages=[]
eventNodes=[] eventNodes=[]
# configuration for events and reputation are json objects
# everything else is considered to be an idle message that
# is randomly being sent into the configured channel
async for message in messages: async for message in messages:
messageContent:str messageContent:str
messageContent=message.content messageContent=message.content
try: try:
# if the message is a dictionary then it is either an event
# or a list of reputation roles
someDict=ast.literal_eval(messageContent) someDict=ast.literal_eval(messageContent)
if isinstance(someDict, dict): if isinstance(someDict, dict):
try:
if (someDict["REPUTATION"] is not None):
allKeys=someDict["REPUTATION"]
# the "MUTE" key contains the role that is assigned to a user
# who sent too many messages and ignored the warnings
self.guildDataList[f'{theGuild.id}'].ReputationFilter = Reputation(theGuild,40,allKeys["MUTE"])
for rep in allKeys:
if (rep != "MUTE" ):
# other roles increaese the reputation
print("Role ", str(rep)," adds Reputation ",allKeys[rep])
self.guildDataList[f'{theGuild.id}'].ReputationFilter.addReputationRole(int(rep),int(allKeys[rep]))
except Exception as ee:
eventNodes.append(someDict) eventNodes.append(someDict)
except Exception as e: except Exception as e:
self.guildDataList[f'{theGuild.id}'].idle_messages.append(message.content) self.guildDataList[f'{theGuild.id}'].idle_messages.append(message.content)
# the "AUTO_EVENTS" node contains all the planned events
guildNode["AUTO_EVENTS"]=eventNodes guildNode["AUTO_EVENTS"]=eventNodes
self.configData.writeGuild(theGuild.id,guildNode) self.configData.writeGuild(theGuild.id,guildNode)
@@ -348,9 +394,17 @@ class OMFBot(AutoShardedBot):
if message.author == self.user: if message.author == self.user:
return return
reputationFilter=self.guildDataList[f'{message.guild.id}'].ReputationFilter
if not (reputationFilter is None):
theScore=await reputationFilter.checkMessage(message)
# user is throtteled or muted
if (theScore<=0):
await message.reply("Hold your horses - **you are sending too many messages**\nHave you sent a lot of links?\nIf you continue to send then **you will be muted**\n**contact an admin** in order to have you added to a trusted role\n")
if (theScore<=-1):
await message.reply("**You have sent too many messages**\nHave you sent a lot of links?\n**you are now muted**\nAdmins have been alerted and will examine the situation\nIf this was a false alert then you will be unblocked soon")
# reset the idle timer if a message has been sent or received # reset the idle timer if a message has been sent or received
self.guildDataList[f'{message.guild.id}'].channel_idle_timer=0 self.guildDataList[f'{message.guild.id}'].channel_idle_timer=0
await self.process_commands(message) await self.process_commands(message)
+183
View File
@@ -0,0 +1,183 @@
import string
import discord
from dataclasses import dataclass
import datetime
# The impact of a role on reputation
@dataclass
class ReputationRole:
roleid:int
score:int
# what we retain from a message
# the Content, if it contains a link and the user who sent the message
@dataclass
class MessageData:
Messagecontent:string
user:discord.Member
# ##########################################################
# Reputation class
# the class that manages the user's reputations.
# Each user has an initial reputation
# that is influenced by the time he/she is on the server
# and roles that are assigned to the user
# (e.g. VIPs can have a high reputation, new users a very
# low one)
# each message that a user sends gives a penalty on the reputation
# each link gives a higher penalty
# only the last x messages are evaluated, i.e. after a certain
# amount of messages the reputation goes back to the initial
# stage (unless you had been muted in between, then you need
# an admin to unblock)
# ##########################################################
class Reputation:
# the initialReputation you have
initialReputation=10
# the penalty in reputation for posting a link
linkPenalty=-5
# the penalty in reputation for posting a message
messagePenalty=-1
# the reputation threshold that will throttle a user
throttleThreshold=5
# the reputation threshold that will mute a user
muteThreshold=0
# the number of messages that one can send in the buffer timeframe that will trigger checks
triggerCheckThreshold=1
# other objects that we may want to refer to
# the bot that called us
theGuild:discord.Guild
# the number of last seen messages which we keep in a ring buffer
messageBufferSize:int
# this is the list of the last messages
lastMessages = []
messageRingBufferCounter:int
# this is the list of the reputation relevant roles
reputationRoles = {}
muteRole:discord.Role
def __init__(self,
theGuild:discord.Guild,
messageBufferSize:int,
muteRole:int) -> None:
self.theGuild=theGuild
self.messageBufferSize=messageBufferSize
self.messageRingBufferCounter=0
for role in theGuild.roles:
if int(muteRole) == int(role.id):
self.muteRole=role
# ##########################################################
# getReputation returns the reputation value for a given
# user either as an int value or as a human readable
# text version
# ##########################################################
def getReputation(self,discordUser:discord.Member,textValue:bool):
# users get initial reputation based on age
now=datetime.datetime.now().replace(tzinfo=None)
userJoin=discordUser.joined_at.replace(tzinfo=None)
userAge = now - userJoin
theReputation=userAge.days
theReputationText="Your Reputation:\n\nInitial (age): " + str(theReputation) + "\n"
# calculate user's score based on reputation roles he/she is in
r:ReputationRole
for r in self.reputationRoles:
theRole=discordUser.get_role(r)
if not (theRole is None):
theReputation += self.reputationRoles[r].score
theReputationText += "Role " + theRole.name + ": " + str(self.reputationRoles[r].score) + "\n"
# add the penalties from the message buffer
messageCount=0
linkCount=0
m:MessageData
for m in self.lastMessages:
if discordUser == m.user:
messageCount +=1
if ('://' in m.Messagecontent):
linkCount +=1
# calculate the remaining reputation credits and construct
# the human readable version
theReputation = theReputation + messageCount * self.messagePenalty
theReputationText += "Message penalty: " + str(messageCount * self.messagePenalty) + "\n"
theReputation = theReputation + linkCount * self.linkPenalty
theReputationText += "Link usage penalty: " + str(linkCount * self.linkPenalty) + "\n"
theReputationText += "\nTotal Reputation: " + str(theReputation) + "\n"
if textValue == True:
return(theReputationText)
else:
return(theReputation)
# ##########################################################
# addReputationRole adds a role that has reputation impact
# to the class
# ##########################################################
def addReputationRole(self,discordRoleID:int,roleScore:int):
theNewRole=ReputationRole(discordRoleID,roleScore)
self.reputationRoles[discordRoleID]=theNewRole
# ##########################################################
# checkMessage analyzes the Message ring buffer and checks
# the user's remaining reputation credits
# If the user reaches the Throttle limit then the user will
# be warned after each message.
# ignoring the warnings will lead to a MUTE role being
# assigned to the user
# ##########################################################
async def checkMessage(self,newMessage:discord.Message):
hasLink=False
theNewMessage=MessageData(newMessage.content,newMessage.author)
# add the new message to the ring buffer
if len(self.lastMessages) < self.messageBufferSize:
self.lastMessages.append(theNewMessage)
else:
self.lastMessages[self.messageRingBufferCounter]=theNewMessage
self.messageRingBufferCounter +=1
if self.messageRingBufferCounter >= self.messageBufferSize:
self.messageRingBufferCounter =0
# count the number of messages for that user in the ring buffer in order
# to evaluate if we need to do a reputation check
m:MessageData
userMessageCount = sum(1 for m in self.lastMessages if m.user == newMessage.author)
if userMessageCount >= self.triggerCheckThreshold:
r = self.getReputation(newMessage.author,False)
if r <= self.muteThreshold:
await newMessage.author.add_roles(self.muteRole)
return(-1)
if r <= self.throttleThreshold: return(0)
return(1)