소스 검색

- Autokick optionally bans after N rejoins

- Bot messages with lists of users are capped at 20 names to avoid 4kb request limit
- Redundant warnings should ping mods less
master
Rocketsoup 2 년 전
부모
커밋
db3a5bcdd9

+ 5
- 0
config.py.sample 파일 보기

@@ -12,7 +12,12 @@ CONFIG = {
12 12
 	'info_emoji': 'ℹ️',
13 13
 	'ignore_emoji': '👍',
14 14
 	'config_path': 'config/',
15
+	'max_members_per_message': 20,
16
+	'squelch_warning_seconds': 300,
15 17
 	'cog_defaults': {
18
+		'AutoKickCog': {
19
+			'bancount': 0
20
+		},
16 21
 		'CrossPostCog': {
17 22
 			'enabled': False,
18 23
 			'warncount': 3,

+ 74
- 9
rocketbot/cogs/autokickcog.py 파일 보기

@@ -1,10 +1,26 @@
1
-import weakref
1
+from datetime import datetime, timedelta
2 2
 
3 3
 from discord import Guild, Member
4 4
 from discord.ext import commands
5 5
 
6 6
 from config import CONFIG
7
-from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
7
+from rocketbot.cogs.basecog import BaseCog, BotMessage, CogSetting
8
+from rocketbot.collections import AgeBoundDict
9
+from rocketbot.storage import Storage
10
+
11
+class AutoKickContext:
12
+	"""
13
+	Data about a join raid.
14
+	"""
15
+	def __init__(self, member: Member, first_kick: datetime):
16
+		self.member = member
17
+		self.first_kick = first_kick
18
+		self.last_kick = first_kick
19
+		self.kick_count = 1
20
+
21
+	def record_kick(self, time: datetime):
22
+		self.last_kick = time
23
+		self.kick_count += 1
8 24
 
9 25
 class AutoKickCog(BaseCog, name='Auto Kick'):
10 26
 	"""
@@ -13,10 +29,20 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
13 29
 	SETTING_ENABLED = CogSetting('enabled', bool,
14 30
 		brief='autokick',
15 31
 		description='Whether this cog is enabled for a guild.')
32
+	SETTING_BAN_COUNT = CogSetting('bancount', int,
33
+			brief='number of repeat kicks before a ban',
34
+			description='The number of times a user can join and be kicked ' + \
35
+					'before the next rejoin will result in a ban. A value of 0 ' + \
36
+					'disables this feature (only kick, never ban).',
37
+			usage='<count:int>',
38
+			min_value=0)
39
+
40
+	STATE_KEY_RECENT_KICKS = "AutoKickCog.recent_joins"
16 41
 
17 42
 	def __init__(self, bot):
18 43
 		super().__init__(bot)
19 44
 		self.add_setting(AutoKickCog.SETTING_ENABLED)
45
+		self.add_setting(AutoKickCog.SETTING_BAN_COUNT)
20 46
 
21 47
 	@commands.group(
22 48
 		brief='Automatically kicks all new users as soon as they join',
@@ -34,10 +60,49 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
34 60
 		guild: Guild = member.guild
35 61
 		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
36 62
 			return
37
-		await member.kick(reason=f'Rocketbot: Autokick enabled.')
38
-		msg = BotMessage(guild,
39
-						text=f'Autokicked {member.mention} ({member.id}). To disable this feature: `{CONFIG["command_prefix"]}autokick disable`.',
40
-						type=BotMessage.TYPE_INFO,
41
-						context=None)
42
-		await self.post_message(msg)
43
-		self.log(guild, f'Autokicked {member.name}')
63
+		recent_kicks: AgeBoundDict = Storage.get_state_value(guild, AutoKickCog.STATE_KEY_RECENT_KICKS)
64
+		if recent_kicks is None:
65
+			recent_kicks = AgeBoundDict(timedelta(seconds=3600), lambda i, context : context.last_kick)
66
+			Storage.set_state_value(guild, self.STATE_KEY_RECENT_KICKS, recent_kicks)
67
+		context: AutoKickContext = recent_kicks.get(member.id)
68
+		if context is None:
69
+			context = AutoKickContext(member, datetime.now())
70
+			recent_kicks[member.id] = context
71
+		else:
72
+			context.record_kick(datetime.now())
73
+		max_kick_count: int = self.get_guild_setting(guild, self.SETTING_BAN_COUNT)
74
+		disable_help = f'To disable this feature: `{CONFIG["command_prefix"]}autokick disable`.'
75
+		ban_help = f'To configure ban threshold: `{CONFIG["command_prefix"]}autokick ' + \
76
+			'setbancount #` (0 to disable).'
77
+		if max_kick_count > 0 and context.kick_count > max_kick_count:
78
+			await member.ban(reason=f'Rocketbot: Ban after {context.kick_count} joins',
79
+				delete_message_days=0)
80
+			msg = BotMessage(guild,
81
+				text=f'Banned {member.mention} ({member.id}) after {context.kick_count} joins. ' + \
82
+					disable_help + ' ' + ban_help,
83
+				type=BotMessage.TYPE_INFO,
84
+				context=None)
85
+			await self.post_message(msg)
86
+			self.log(guild, f'Banned {member.name} after {context.kick_count} joins')
87
+		else:
88
+			await member.kick(reason='Rocketbot: Autokick enabled.')
89
+			msg = BotMessage(guild,
90
+							text=f'Autokicked {member.mention} ({member.id}) ' + \
91
+								f'({AutoKickCog.ordinal(context.kick_count)} time). ' + \
92
+								disable_help + ' ' + ban_help,
93
+							type=BotMessage.TYPE_INFO,
94
+							context=None)
95
+			await self.post_message(msg)
96
+			self.log(guild, f'Autokicked {member.name} ' + \
97
+				f'({AutoKickCog.ordinal(context.kick_count)} time)')
98
+
99
+	@staticmethod
100
+	def ordinal(val: int):
101
+		'Formats an integer with an ordinal suffix (English only)'
102
+		if val % 10 == 1:
103
+			return f'{val}st'
104
+		if val % 10 == 2:
105
+			return f'{val}nd'
106
+		if val % 10 == 3:
107
+			return f'{val}rd'
108
+		return f'{val}th'

+ 54
- 0
rocketbot/cogs/basecog.py 파일 보기

@@ -13,7 +13,14 @@ from rocketbot.collections import AgeBoundDict
13 13
 from rocketbot.storage import Storage
14 14
 from rocketbot.utils import bot_log
15 15
 
16
+class WarningContext:
17
+	def __init__(self, member: Member, warn_time: datetime):
18
+		self.member = member
19
+		self.last_warned = warn_time
20
+
16 21
 class BaseCog(commands.Cog):
22
+	STATE_KEY_RECENT_WARNINGS = "BaseCog.recent_warnings"
23
+
17 24
 	"""
18 25
 	Superclass for all Rocketbot cogs. Provides lots of conveniences for
19 26
 	common tasks.
@@ -98,6 +105,53 @@ class BaseCog(commands.Cog):
98 105
 		Subclass override point for being notified when a CogSetting is edited.
99 106
 		"""
100 107
 
108
+	# Warning squelch
109
+
110
+	def was_warned_recently(self, member: Member) -> bool:
111
+		"""
112
+		Tests if a given member was included in a mod warning message recently.
113
+		Used to suppress redundant messages. Should be checked before pinging
114
+		mods for relatively minor warnings about single users, but warnings
115
+		about larger threats involving several members (e.g. join raids) should
116
+		issue warnings regardless. Call record_warning or record_warnings after
117
+		triggering a mod warning.
118
+		"""
119
+		recent_warns: AgeBoundDict = Storage.get_state_value(member.guild,
120
+			BaseCog.STATE_KEY_RECENT_WARNINGS)
121
+		if recent_warns is None:
122
+			return False
123
+		context: WarningContext = recent_warns.get(member.id)
124
+		if context is None:
125
+			return False
126
+		squelch_warning_seconds: int = CONFIG['squelch_warning_seconds']
127
+		return datetime.now - context.last_warned < timedelta(seconds=squelch_warning_seconds)
128
+
129
+	def record_warning(self, member: Member):
130
+		"""
131
+		Records that mods have been warned about a member and do not need to be
132
+		warned about them again for a short while.
133
+		"""
134
+		recent_warns: AgeBoundDict = Storage.get_state_value(member.guild,
135
+			BaseCog.STATE_KEY_RECENT_WARNINGS)
136
+		if recent_warns is None:
137
+			recent_warns = AgeBoundDict(timedelta(seconds=CONFIG['squelch_warning_seconds']),
138
+				lambda i, context : context.last_warned)
139
+			Storage.set_state_value(member.guild, BaseCog.STATE_KEY_RECENT_WARNINGS, recent_warns)
140
+		context: WarningContext = recent_warns.get(member.id)
141
+		if context is None:
142
+			context = WarningContext(member, datetime.now())
143
+			recent_warns[member.id] = context
144
+		else:
145
+			context.last_warned = datetime.now()
146
+
147
+	def record_warnings(self, members: list):
148
+		"""
149
+		Records that mods have been warned about some members and do not need to
150
+		be warned about them again for a short while.
151
+		"""
152
+		for member in members:
153
+			self.record_warning(member)
154
+
101 155
 	# Bot message handling
102 156
 
103 157
 	@classmethod

+ 4
- 2
rocketbot/cogs/crosspostcog.py 파일 보기

@@ -169,9 +169,11 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
169 169
 		deleted_count = len(context.spam_messages)
170 170
 		message = context.bot_message
171 171
 		if message is None:
172
-			message = BotMessage(context.member.guild, '',
173
-				BotMessage.TYPE_MOD_WARNING, context)
172
+			message_type: int = BotMessage.TYPE_INFO if self.was_warned_recently(context.member) \
173
+				else BotMessage.TYPE_MOD_WARNING
174
+			message = BotMessage(context.member.guild, '', message_type, context)
174 175
 			message.quote = discordutils.remove_markdown(first_spam_message.clean_content)
176
+			self.record_warning(context.member)
175 177
 		if context.is_autobanned:
176 178
 			text = f'User {context.member.mention} auto banned for ' + \
177 179
 				f'posting the same message in {channel_count} channels. ' + \

+ 4
- 1
rocketbot/cogs/joinagecog.py 파일 보기

@@ -125,7 +125,8 @@ class JoinAgeCog(BaseCog, name='Join Age'):
125 125
 			return
126 126
 		text = f'The following members joined in the last {context.timespan}\n\n'
127 127
 		if len(context.join_members) > 0:
128
-			for member in context.join_members:
128
+			max_members = CONFIG['max_members_per_message']
129
+			for member in context.join_members[:max_members]:
129 130
 				text += '\n• '
130 131
 				if member in context.banned_members:
131 132
 					text += f'~~{member.mention} ({member.id})~~ - banned'
@@ -133,6 +134,8 @@ class JoinAgeCog(BaseCog, name='Join Age'):
133 134
 					text += f'~~{member.mention} ({member.id})~~ - kicked'
134 135
 				else:
135 136
 					text += f'{member.mention} ({member.id})'
137
+			if len(context.join_members) > max_members:
138
+				text += f'\n• {len(context.join_members) - max_members} more'
136 139
 		else:
137 140
 			text += 'No members found. If the bot was recently restarted ' + \
138 141
 				'or JoinAgeCog just enabled, only new joins will be tracked.'

+ 8
- 3
rocketbot/cogs/joinraidcog.py 파일 보기

@@ -113,16 +113,17 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
113 113
 				return
114 114
 			# Add join to existing raid
115 115
 			last_raid.join_members.append(member)
116
+			self.record_warning(member)
116 117
 			if len(last_raid.banned_members) > 0:
117 118
 				self.log(guild, f'Banning as part of last join raid: {member.name}')
118 119
 				await member.ban(
119
-					reason=f'Rocketbot: Part of join raid.',
120
+					reason='Rocketbot: Part of join raid.',
120 121
 					delete_message_days=0)
121 122
 				last_raid.banned_members.add(member)
122 123
 			elif len(last_raid.kicked_members) > 0:
123 124
 				self.log(guild, f'Kicking as part of last join raid: {member.name}')
124 125
 				await member.kick(
125
-					reason=f'Rocketbot: Part of join raid.')
126
+					reason='Rocketbot: Part of join raid.')
126 127
 				last_raid.kicked_members.add(member)
127 128
 			await self.__update_warning_message(last_raid)
128 129
 		else:
@@ -137,6 +138,7 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
137 138
 						text='',
138 139
 						type=BotMessage.TYPE_MOD_WARNING,
139 140
 						context=last_raid)
141
+				self.record_warnings(recent_joins)
140 142
 				last_raid.warning_message_ref = weakref.ref(msg)
141 143
 				await self.__update_warning_message(last_raid)
142 144
 				await self.post_message(msg)
@@ -159,7 +161,8 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
159 161
 			return
160 162
 		text = 'JOIN RAID DETECTED\n\n' + \
161 163
 			'The following members joined in close succession:\n'
162
-		for member in context.join_members:
164
+		max_members: int = CONFIG['max_members_per_message']
165
+		for member in context.join_members[:max_members]:
163 166
 			text += '\n• '
164 167
 			if member in context.banned_members:
165 168
 				text += f'~~{member.mention} ({member.id})~~ - banned'
@@ -167,6 +170,8 @@ class JoinRaidCog(BaseCog, name='Join Raids'):
167 170
 				text += f'~~{member.mention} ({member.id})~~ - kicked'
168 171
 			else:
169 172
 				text += f'{member.mention} ({member.id})'
173
+		if len(context.join_members) > max_members:
174
+			text += f'\n• {len(context.join_members) - max_members} more'
170 175
 		text += '\n_(list updates automatically)_'
171 176
 		await bot_message.set_text(text)
172 177
 		member_count = len(context.join_members)

+ 2
- 1
rocketbot/cogs/patterncog.py 파일 보기

@@ -138,7 +138,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
138 138
 				message_type = BotMessage.TYPE_INFO
139 139
 				action_descriptions.append('Message logged')
140 140
 			elif action.action == 'modwarn':
141
-				should_post_message = True
141
+				should_post_message = not self.was_warned_recently(message.author)
142 142
 				message_type = BotMessage.TYPE_MOD_WARNING
143 143
 				action_descriptions.append('Mods alerted')
144 144
 			elif action.action == 'reply':
@@ -155,6 +155,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
155 155
 					('\n• '.join(action_descriptions)),
156 156
 				type=message_type,
157 157
 				context=context)
158
+			self.record_warning(message.author)
158 159
 			bm.quote = discordutils.remove_markdown(message.clean_content)
159 160
 			await bm.set_reactions(BotMessageReaction.standard_set(
160 161
 				did_delete=context.is_deleted,

+ 4
- 2
rocketbot/cogs/urlspamcog.py 파일 보기

@@ -85,10 +85,10 @@ class URLSpamCog(BaseCog, name='URL Spam'):
85 85
 			context = URLSpamContext(message)
86 86
 			needs_attention = False
87 87
 			if action == 'modwarn':
88
-				needs_attention = True
88
+				needs_attention = not self.was_warned_recently(message.author)
89 89
 				self.log(message.guild, f'New user {message.author.name} ' + \
90 90
 					f'({message.author.id}) posted URL {join_age_str} after ' + \
91
-					'joining. Mods alerted.')
91
+					'joining.' + (' Mods alerted.' if needs_attention else ''))
92 92
 			elif action == 'delete':
93 93
 				await message.delete()
94 94
 				context.is_deleted = True
@@ -126,6 +126,8 @@ class URLSpamCog(BaseCog, name='URL Spam'):
126 126
 				did_kick=context.is_kicked,
127 127
 				did_ban=context.is_banned))
128 128
 			await self.post_message(bm)
129
+			if needs_attention:
130
+				self.record_warning(message.author)
129 131
 
130 132
 	async def on_mod_react(self,
131 133
 			bot_message: BotMessage,

+ 2
- 1
rocketbot/cogs/usernamecog.py 파일 보기

@@ -156,8 +156,9 @@ class UsernamePatternCog(BaseCog, name='Username Pattern'):
156 156
 			member.guild,
157 157
 			f'User {member.mention} ({str(member.id)}, {member.display_name}) has ' +
158 158
 			f'username matching pattern `{pattern}`.',
159
-			BotMessage.TYPE_MOD_WARNING,
159
+			BotMessage.TYPE_INFO if self.was_warned_recently(member) else BotMessage.TYPE_MOD_WARNING,
160 160
 			context)
161
+		self.record_warning(member)
161 162
 		await bm.set_reactions(context.reactions())
162 163
 		await self.post_message(bm)
163 164
 

Loading…
취소
저장