added reputation filter
Signed-off-by: Marc Ahlgrim <marc@onemarcfifty.com>
This commit is contained in:
+57
-3
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user