소스 검색

New bot message system. Squelching joinraid and storage log noise. Join raids beep in console.

tags/1.0.1
Rocketsoup 4 년 전
부모
커밋
a98521aac1
6개의 변경된 파일334개의 추가작업 그리고 103개의 파일을 삭제
  1. 248
    25
      cogs/basecog.py
  2. 71
    59
      cogs/crosspostcog.py
  3. 7
    13
      cogs/joinraidcog.py
  4. 5
    5
      cogs/urlspamcog.py
  5. 1
    0
      config.py.sample
  6. 2
    1
      storage.py

+ 248
- 25
cogs/basecog.py 파일 보기

@@ -1,16 +1,221 @@
1
-from discord import Guild, Message, PartialEmoji, RawReactionActionEvent, TextChannel
1
+from discord import Guild, Member, Message, PartialEmoji, RawReactionActionEvent, TextChannel
2 2
 from discord.ext import commands
3
-from datetime import timedelta
3
+from datetime import datetime, timedelta
4 4
 
5 5
 from config import CONFIG
6 6
 from rscollections import AgeBoundDict
7 7
 from storage import ConfigKey, Storage
8 8
 import json
9 9
 
10
+class BotMessageReaction:
11
+	"""
12
+	A possible reaction to a bot message that will trigger an action. The list
13
+	of available reactions will be listed at the end of a BotMessage. When a
14
+	mod reacts to the message with the emote, something can happen.
15
+
16
+	If the reaction is disabled, reactions will not register. The description
17
+	will still show up in the message, but no emoji is shown. This can be used
18
+	to explain why an action is no longer available.
19
+	"""
20
+	def __init__(self, emoji: str, is_enabled: bool, description: str):
21
+		self.emoji = emoji
22
+		self.is_enabled = is_enabled
23
+		self.description = description
24
+
25
+	def __eq__(self, other):
26
+		return other is not None and \
27
+			other.emoji == self.emoji and \
28
+			other.is_enabled == self.is_enabled and \
29
+			other.description == self.description
30
+
31
+class BotMessage:
32
+	"""
33
+	Holds state for a bot-generated message. A message is composed, sent via
34
+	`BaseCog.post_message()`, and can later be updated.
35
+
36
+	A message consists of a type (e.g. info, warning), text, optional quoted
37
+	text (such as the content of a flagged message), and an optional list of
38
+	actions that can be taken via a mod reacting to the message.
39
+	"""
40
+
41
+	TYPE_DEFAULT = 0
42
+	TYPE_INFO = 1
43
+	TYPE_MOD_WARNING = 2
44
+	TYPE_SUCCESS = 3
45
+	TYPE_FAILURE = 4
46
+
47
+	def __init__(self,
48
+			guild: Guild,
49
+			text: str,
50
+			type: int = 0, # TYPE_DEFAULT
51
+			context = None,
52
+			reply_to: Message = None):
53
+		self.guild = guild
54
+		self.text = text
55
+		self.type = type
56
+		self.context = context
57
+		self.quote = None
58
+		self.__posted_text = None  # last text posted, to test for changes
59
+		self.__posted_emoji = set()
60
+		self.__message = None  # Message
61
+		self.__reply_to = reply_to
62
+		self.__reactions = []  # BotMessageReaction[]
63
+
64
+	def is_sent(self) -> bool:
65
+		"""
66
+		Returns whether this message has been sent to the guild. This may
67
+		continue returning False even after calling BaseCog.post_message if
68
+		the guild has no configured warning channel.
69
+		"""
70
+		return self.__message is not None
71
+
72
+	def message_id(self):
73
+		return self.__message.id if self.__message else None
74
+
75
+	def message_sent_at(self):
76
+		return self.__message.created_at if self.__message else None
77
+
78
+	async def set_text(self, new_text: str) -> None:
79
+		"""
80
+		Replaces the text of this message. If the message has been sent, it will
81
+		be updated.
82
+		"""
83
+		self.text = new_text
84
+		await self.__update_if_sent()
85
+
86
+	async def set_reactions(self, reactions: list) -> None:
87
+		"""
88
+		Replaces all BotMessageReactions with a new list. If the message has
89
+		been sent, it will be updated.
90
+		"""
91
+		if reactions == self.__reactions:
92
+			# No change
93
+			return
94
+		self.__reactions = reactions.copy() if reactions is not None else []
95
+		await self.__update_if_sent()
96
+
97
+	async def add_reaction(self, reaction: BotMessageReaction) -> None:
98
+		"""
99
+		Adds one BotMessageReaction to this message. If a reaction already
100
+		exists for the given emoji it is replaced with the new one. If the
101
+		message has been sent, it will be updated.
102
+		"""
103
+		# Alias for update. Makes for clearer intent.
104
+		await self.update_reaction(reaction)
105
+
106
+	async def update_reaction(self, reaction: BotMessageReaction) -> None:
107
+		"""
108
+		Updates or adds a BotMessageReaction. If the message has been sent, it
109
+		will be updated.
110
+		"""
111
+		found = False
112
+		for i in range(len(self.__reactions)):
113
+			existing = self.__reactions[i]
114
+			if existing.emoji == reaction.emoji:
115
+				if reaction == self.__reactions[i]:
116
+					# No change
117
+					return
118
+				self.__reactions[i] = reaction
119
+				found = True
120
+				break
121
+		if not found:
122
+			self.__reactions.append(reaction)
123
+		await self.__update_if_sent()
124
+
125
+	async def remove_reaction(self, reaction_or_emoji) -> None:
126
+		"""
127
+		Removes a reaction. Can pass either a BotMessageReaction or just the
128
+		emoji string. If the message has been sent, it will be updated.
129
+		"""
130
+		for i in range(len(self.__reactions)):
131
+			existing = self.__reactions[i]
132
+			if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
133
+				(isinstance(reaction_or_emoji, BotMessageReaction) and existing.emoji == reaction_or_emoji.emoji):
134
+				self.__reactions.pop(i)
135
+				await self.__update_if_sent()
136
+				return
137
+
138
+	def reaction_for_emoji(self, emoji) -> BotMessageReaction:
139
+		for reaction in self.__reactions:
140
+			if isinstance(emoji, PartialEmoji) and reaction.emoji == emoji.name:
141
+				return reaction
142
+			elif isinstance(emoji, str) and reaction.emoji == emoji:
143
+				return reaction
144
+		return None
145
+
146
+	async def __update_if_sent(self) -> None:
147
+		if self.__message:
148
+			await self._update()
149
+
150
+	async def _update(self) -> None:
151
+		content: str = self.__formatted_message()
152
+		if self.__message:
153
+			if content != self.__posted_text:
154
+				await self.__message.edit(content=content)
155
+				self.__posted_text = content
156
+		else:
157
+			if self.__reply_to:
158
+				self.__message = await self.__reply_to.reply(content=content, mention_author=False)
159
+				self.__posted_text = content
160
+			else:
161
+				channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
162
+				if channel_id is None:
163
+					BaseCog.guild_trace(self.guild, 'No warning channel set! No warning issued.')
164
+					return
165
+				channel: TextChannel = self.guild.get_channel(channel_id)
166
+				if channel is None:
167
+					BaseCog.guild_trace(self.guild, 'Configured warning channel does not exist!')
168
+					return
169
+				self.__message = await channel.send(content=content)
170
+				self.__posted_text = content
171
+		emoji_to_remove = self.__posted_emoji.copy()
172
+		for reaction in self.__reactions:
173
+			if reaction.is_enabled:
174
+				if reaction.emoji not in self.__posted_emoji:
175
+					await self.__message.add_reaction(reaction.emoji)
176
+					self.__posted_emoji.add(reaction.emoji)
177
+				if reaction.emoji in emoji_to_remove:
178
+					emoji_to_remove.remove(reaction.emoji)
179
+		for emoji in emoji_to_remove:
180
+			await self.__message.clear_reaction(emoji)
181
+			if emoji in self.__posted_emoji:
182
+				self.__posted_emoji.remove(emoji)
183
+
184
+	def __formatted_message(self) -> str:
185
+		s: str = ''
186
+
187
+		if self.type == self.TYPE_INFO:
188
+			s += CONFIG['info_emoji'] + ' '
189
+		elif self.type == self.TYPE_MOD_WARNING:
190
+			mention: str = Storage.get_config_value(self.guild, ConfigKey.WARNING_MENTION)
191
+			if mention:
192
+				s += mention + ' '
193
+			s += CONFIG['warning_emoji'] + ' '
194
+		elif self.type == self.TYPE_SUCCESS:
195
+			s += CONFIG['success_emoji'] + ' '
196
+		elif self.type == self.TYPE_FAILURE:
197
+			s += CONFIG['failure_emoji'] + ' '
198
+
199
+		s += self.text
200
+
201
+		if self.quote:
202
+			s += f'\n\n> {self.quote}'
203
+
204
+		if len(self.__reactions) > 0:
205
+			s += '\n\nAvailable actions:'
206
+			for reaction in self.__reactions:
207
+				if reaction.is_enabled:
208
+					s += f'\n   {reaction.emoji} {reaction.description}'
209
+				else:
210
+					s += f'\n   {reaction.description}'
211
+
212
+		return s
213
+
10 214
 class BaseCog(commands.Cog):
11 215
 	def __init__(self, bot):
12 216
 		self.bot = bot
13
-		self.listened_mod_react_message_ids = AgeBoundDict(timedelta(minutes=5), lambda message_id, tpl : tpl[0])
217
+
218
+	# Config
14 219
 
15 220
 	@classmethod
16 221
 	def get_cog_default(cls, key: str):
@@ -24,14 +229,26 @@ class BaseCog(commands.Cog):
24 229
 			return None
25 230
 		return cog.get(key)
26 231
 
27
-	def listen_for_reactions_to(self, message: Message, context = None) -> None:
28
-		"""
29
-		Registers a warning message as something a mod may react to to enact
30
-		some action. `context` will be passed back in `on_mod_react` and can be
31
-		any value that helps give the cog context about the action being
32
-		performed.
33
-		"""
34
-		self.listened_mod_react_message_ids[message.id] = (message.created_at, context)
232
+	# Bot message handling
233
+
234
+	@classmethod
235
+	def __bot_messages(cls, guild: Guild) -> AgeBoundDict:
236
+		bm = Storage.get_state_value(guild, 'bot_messages')
237
+		if bm is None:
238
+			far_future = datetime.utcnow() + timedelta(days=1000)
239
+			bm = AgeBoundDict(timedelta(seconds=600),
240
+				lambda k, v : v.message_sent_at() or far_future)
241
+			Storage.set_state_value(guild, 'bot_messages', bm)
242
+		return bm
243
+
244
+	@classmethod
245
+	async def post_message(cls, message: BotMessage) -> bool:
246
+		await message._update()
247
+		guild_messages = cls.__bot_messages(message.guild)
248
+		if message.is_sent():
249
+			guild_messages[message.message_id()] = message
250
+			return True
251
+		return False
35 252
 
36 253
 	@commands.Cog.listener()
37 254
 	async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
@@ -57,27 +274,32 @@ class BaseCog(commands.Cog):
57 274
 		if message.author.id != self.bot.user.id:
58 275
 			# Bot didn't author this
59 276
 			return
60
-		if not member.permissions_in(channel).ban_members:
61
-			# Not a mod
277
+		guild_messages = self.__bot_messages(guild)
278
+		bot_message = guild_messages.get(message.id)
279
+		if bot_message is None:
280
+			# Unknown message (expired or was never tracked)
62 281
 			return
63
-		tpl = self.listened_mod_react_message_ids.get(message.id)
64
-		if tpl is None:
65
-			# Not a message we're listening for
282
+		reaction = bot_message.reaction_for_emoji(payload.emoji)
283
+		if reaction is None or not reaction.is_enabled:
284
+			# Can't use this reaction with this message
66 285
 			return
67
-		context = tpl[1]
68
-		await self.on_mod_react(message, payload.emoji, context)
286
+		if not member.permissions_in(channel).ban_members:
287
+			# Not a mod (could make permissions configurable per BotMessageReaction some day)
288
+			return
289
+		await self.on_mod_react(bot_message, reaction, member)
69 290
 
70
-	async def on_mod_react(self, message: Message, emoji: PartialEmoji, context) -> None:
291
+	async def on_mod_react(self,
292
+			bot_message: BotMessage,
293
+			reaction: BotMessageReaction,
294
+			reacted_by: Member) -> None:
71 295
 		"""
72
-		Override point for getting a mod's emote on a bot message. Used to take
73
-		action on a warning, such as banning an offending user. This event is
74
-		only triggered for registered bot messages and reactions by members
75
-		with the proper permissions. The given `context` value is whatever was
76
-		passed in `listen_to_reactions_to()`.
296
+		Subclass override point for receiving mod reactions to bot messages sent
297
+		via `post_message()`.
77 298
 		"""
78 299
 		pass
79 300
 
80
-	async def validate_param(self, context: commands.Context, param_name: str, value,
301
+	@classmethod
302
+	async def validate_param(cls, context: commands.Context, param_name: str, value,
81 303
 		allowed_types: tuple = None,
82 304
 		min_value = None,
83 305
 		max_value = None) -> bool:
@@ -87,6 +309,7 @@ class BaseCog(commands.Cog):
87 309
 		to the original message and a False will be returned. If all checks
88 310
 		succeed, True will be returned.
89 311
 		"""
312
+		# TODO: Rework this to use BotMessage
90 313
 		if allowed_types is not None and not isinstance(value, allowed_types):
91 314
 			if len(allowed_types) == 1:
92 315
 				await context.message.reply(f'⚠️ `{param_name}` must be of type ' +

+ 71
- 59
cogs/crosspostcog.py 파일 보기

@@ -1,11 +1,11 @@
1
-from discord import Guild, Message, PartialEmoji
1
+from discord import Guild, Member, Message, PartialEmoji
2 2
 from discord.ext import commands
3 3
 from datetime import datetime, timedelta
4 4
 import math
5 5
 
6 6
 from config import CONFIG
7 7
 from rscollections import AgeBoundList, SizeBoundDict
8
-from cogs.basecog import BaseCog
8
+from cogs.basecog import BaseCog, BotMessage, BotMessageReaction
9 9
 from storage import Storage
10 10
 
11 11
 class SpamContext:
@@ -13,11 +13,12 @@ class SpamContext:
13 13
 		self.member = member
14 14
 		self.message_hash = message_hash
15 15
 		self.age = datetime.now()
16
-		self.warning_message = None
16
+		self.bot_message = None  # BotMessage
17 17
 		self.is_kicked = False
18 18
 		self.is_banned = False
19
-		self.messages = set()
20
-		self.deleted_messages = set()
19
+		self.is_autobanned = False
20
+		self.spam_messages = set()  # of Message
21
+		self.deleted_messages = set()  # of Message
21 22
 
22 23
 class CrossPostCog(BaseCog):
23 24
 	STATE_KEY_RECENT_MESSAGES = "crosspost_recent_messages"
@@ -101,63 +102,70 @@ class CrossPostCog(BaseCog):
101 102
 				context.age = message.created_at
102 103
 			for m in member_messages:
103 104
 				if hash(m.content) == message_hash:
104
-					context.messages.add(m)
105
+					context.spam_messages.add(m)
105 106
 			await self.__update_from_context(context)
106 107
 
107 108
 	async def __update_from_context(self, context: SpamContext):
108
-		content = next(iter(context.messages)).content
109
-		if len(context.messages) >= self.__ban_count(context.member.guild):
109
+		if len(context.spam_messages) >= self.__ban_count(context.member.guild):
110 110
 			if not context.is_banned:
111
-				await context.member.ban(reason='Posting same message repeatedly', delete_message_days=1)
112
-				msg = f'User {context.member.mention} auto banned for ' + \
113
-					'crosspost spamming.\n' + \
114
-					f'> {content}'
115
-				if context.warning_message:
116
-					await self.update_warn(context.warning_message, msg)
117
-					await context.warning_message.clear_reaction(CONFIG['trash_emoji'])
118
-					await context.warning_message.clear_reaction(CONFIG['kick_emoji'])
119
-					await context.warning_message.clear_reaction(CONFIG['ban_emoji'])
120
-				else:
121
-					context.warning_message = await self.warn(context.member.guild, msg)
122
-		elif len(context.messages) >= self.__warn_count(context.member.guild):
123
-			content = next(iter(context.messages)).content
124
-			msg = f'User {context.member.mention} has posted the exact same ' + \
125
-				f'message {len(context.messages)} times.\n' + \
126
-				f'> {content}' + \
127
-				'\n'
128
-			can_delete = len(context.messages) > len(context.deleted_messages)
129
-			if can_delete:
130
-				msg += f'\n{CONFIG["trash_emoji"]} to delete messages'
111
+				count = len(context.spam_messages)
112
+				await context.member.ban(reason=f'Autobanned by Rocketbot for posting same message {count} times', delete_message_days=1)
113
+				context.is_kicked = True
114
+				context.is_banned = True
115
+				context.is_autobanned = True
116
+				context.deleted_messages |= context.spam_messages
131 117
 			else:
132
-				msg += '\nAll messages deleted'
133
-			if not context.is_kicked:
134
-				msg += f'\n{CONFIG["kick_emoji"]} to kick user'
135
-			elif not context.is_banned:
136
-				msg += '\nUser kicked'
137
-			if context.is_banned:
138
-				msg += '\nUser banned'
139
-			else:
140
-				msg += '\n{CONFIG["ban_emoji"]} to ban user'
141
-			if context.warning_message:
142
-				await self.update_warn(context.warning_message, msg)
143
-			else:
144
-				context.warning_message = await self.warn(context.member.guild, msg)
145
-				self.listen_for_reactions_to(context.warning_message, context)
146
-			if can_delete:
147
-				await context.warning_message.add_reaction(CONFIG['trash_emoji'])
148
-			else:
149
-				await context.warning_message.clear_reaction(CONFIG['trash_emoji'])
150
-			if not context.is_kicked:
151
-				await context.warning_message.add_reaction(CONFIG['kick_emoji'])
152
-			else:
153
-				await context.warning_message.clear_reaction(CONFIG['kick_emoji'])
154
-			if not context.is_banned:
155
-				await context.warning_message.add_reaction(CONFIG['ban_emoji'])
118
+				# Already banned. Nothing to update in the message.
119
+				return
120
+		await self.__update_message_from_context(context)
121
+
122
+	async def __update_message_from_context(self, context: SpamContext) -> None:
123
+		first_spam_message = next(iter(context.spam_messages))
124
+		spam_count = len(context.spam_messages)
125
+		deleted_count = len(context.deleted_messages)
126
+		message = context.bot_message
127
+		if message is None:
128
+			message = BotMessage(context.member.guild, '',
129
+				BotMessage.TYPE_MOD_WARNING, context)
130
+			message.quote = first_spam_message.content
131
+		if context.is_autobanned:
132
+			text = f'User {context.member.mention} auto banned for ' + \
133
+				f'posting the same message {deleted_count} ' + \
134
+				'times. Messages from past 24 hours deleted.'
135
+			await message.set_reactions([])
136
+			await message.set_text(text)
137
+		else:
138
+			await message.set_text(f'User {context.member.mention} posted ' +
139
+				f'the same message {spam_count} times.')
140
+			can_delete = spam_count > deleted_count
141
+			can_kick = not context.is_kicked
142
+			can_ban = not context.is_banned
143
+			await message.add_reaction(
144
+				BotMessageReaction(
145
+					CONFIG['trash_emoji'],
146
+					can_delete,
147
+					'Delete messages' if can_delete else f'Deleted {deleted_count} messages'))
148
+			if can_ban:
149
+				# Only show kick info if they can also be banned. Otherwise we say
150
+				# dumb stuff like "user was kicked, user was banned".
151
+				await message.add_reaction(
152
+					BotMessageReaction(
153
+						CONFIG['kick_emoji'],
154
+						can_kick,
155
+						'Kick user' if can_kick else 'User kicked'))
156 156
 			else:
157
-				await context.warning_message.clear_reaction(CONFIG['ban_emoji'])
157
+				await message.remove_reaction(CONFIG['kick_emoji'])
158
+			await message.add_reaction(
159
+				BotMessageReaction(
160
+					CONFIG['ban_emoji'], 
161
+					can_ban,
162
+					'Ban user' if can_ban else 'User banned'))
163
+		if context.bot_message is None:
164
+			await self.post_message(message)
165
+			context.bot_message = message
158 166
 
159 167
 	async def __delete_messages(self, context: SpamContext) -> None:
160
-		for message in context.messages - context.deleted_messages:
168
+		for message in context.spam_messages - context.deleted_messages:
161 169
 			await message.delete()
162 170
 			context.deleted_messages.add(message)
163 171
 		await self.__update_from_context(context)
@@ -169,20 +177,24 @@ class CrossPostCog(BaseCog):
169 177
 
170 178
 	async def __ban(self, context: SpamContext) -> None:
171 179
 		await context.member.ban(reason='Posting same message repeatedly', delete_message_days=1)
172
-		context.deleted_messages |= context.messages
180
+		context.deleted_messages |= context.spam_messages
173 181
 		context.is_kicked = True
174 182
 		context.is_banned = True
175 183
 		await self.__update_from_context(context)
176 184
 
177
-	async def on_mod_react(self, message: Message, emoji: PartialEmoji, context: SpamContext) -> None:
185
+	async def on_mod_react(self,
186
+			bot_message: BotMessage,
187
+			reaction: BotMessageReaction,
188
+			reacted_by: Member) -> None:
189
+		context: SpamContext = bot_message.context
178 190
 		if context is None:
179 191
 			return
180 192
 
181
-		if emoji.name == CONFIG['trash_emoji']:
193
+		if reaction.emoji == CONFIG['trash_emoji']:
182 194
 			await self.__delete_messages(context)
183
-		elif emoji.name == CONFIG['kick_emoji']:
195
+		elif reaction.emoji == CONFIG['kick_emoji']:
184 196
 			await self.__kick(context)
185
-		elif emoji.name == CONFIG['ban_emoji']:
197
+		elif reaction.emoji == CONFIG['ban_emoji']:
186 198
 			await self.__ban(context)
187 199
 
188 200
 	@commands.Cog.listener()

+ 7
- 13
cogs/joinraidcog.py 파일 보기

@@ -57,13 +57,11 @@ class JoinRaidRecord:
57 57
 		self.phase and self.raid_start_time properties.
58 58
 		"""
59 59
 		# Check for existing record for this user
60
-		print(f'handle_join({member.name}) start')
61 60
 		join: JoinRecord = None
62 61
 		i: int = 0
63 62
 		while i < len(self.joins):
64 63
 			elem = self.joins[i]
65 64
 			if elem.member.id == member.id:
66
-				print(f'Member {member.name} already in join list at index {i}. Removing.')
67 65
 				join = self.joins.pop(i)
68 66
 				join.join_time = now
69 67
 				break
@@ -72,7 +70,6 @@ class JoinRaidRecord:
72 70
 		self.joins.append(join or JoinRecord(member))
73 71
 		# Check raid status and do upkeep
74 72
 		self.__process_joins(now, max_age_seconds, max_join_count)
75
-		print(f'handle_join({member.name}) end')
76 73
 
77 74
 	def __process_joins(self,
78 75
 			now: datetime,
@@ -82,7 +79,6 @@ class JoinRaidRecord:
82 79
 		Processes self.joins after each addition, detects raids, updates
83 80
 		self.phase, and throws out unneeded records.
84 81
 		"""
85
-		print('__process_joins {')
86 82
 		i: int = 0
87 83
 		recent_count: int = 0
88 84
 		should_cull: bool = self.phase == RaidPhase.NONE
@@ -92,32 +88,26 @@ class JoinRaidRecord:
92 88
 			is_old: bool = age > max_age_seconds
93 89
 			if not is_old:
94 90
 				recent_count += 1
95
-				print(f'- {i}. {join.member.name} is {age}s old - recent_count={recent_count}')
96 91
 			if is_old and should_cull:
97 92
 				self.joins.pop(i)
98
-				print(f'- {i}. {join.member.name} is {age}s old - too old, removing')
99 93
 			else:
100
-				print(f'- {i}. {join.member.name} is {age}s old - moving on to next')
101 94
 				i += 1
102 95
 		is_raid = recent_count > max_join_count
103
-		print(f'- is_raid {is_raid}')
104 96
 		if is_raid:
105 97
 			if self.phase == RaidPhase.NONE:
106 98
 				self.phase = RaidPhase.JUST_STARTED
107 99
 				self.raid_start_time = now
108
-				print('- Phase moved to JUST_STARTED. Recording raid start time.')
100
+				print('\u0007Join raid started!')
109 101
 			elif self.phase == RaidPhase.JUST_STARTED:
110 102
 				self.phase = RaidPhase.CONTINUING
111
-				print('- Phase moved to CONTINUING.')
103
+				print(f'\u0007Join raid up to {recent_count} people')
112 104
 		elif self.phase == self.phase in (RaidPhase.JUST_STARTED, RaidPhase.CONTINUING):
113 105
 			self.phase = RaidPhase.ENDED
114
-			print('- Phase moved to ENDED.')
106
+			print('Previous join raid ended')
115 107
 
116 108
 		# Undo join add if the raid is over
117 109
 		if self.phase == RaidPhase.ENDED and len(self.joins) > 0:
118 110
 			last = self.joins.pop(-1)
119
-			print(f'- Popping last join for {last.member.name}')
120
-		print('} __process_joins')
121 111
 
122 112
 	async def kick_all(self,
123 113
 			reason: str = "Part of join raid") -> list[Member]:
@@ -134,6 +124,8 @@ class JoinRaidRecord:
134 124
 			join.is_kicked = True
135 125
 			kicks.append(join.member)
136 126
 		self.phase = RaidPhase.ENDED
127
+		if len(kicks) > 0:
128
+			print(f'Kicked {len(kicks)} people')
137 129
 		return kicks
138 130
 
139 131
 	async def ban_all(self,
@@ -154,6 +146,8 @@ class JoinRaidRecord:
154 146
 			join.is_banned = True
155 147
 			bans.append(join.member)
156 148
 		self.phase = RaidPhase.ENDED
149
+		if len(bans) > 0:
150
+			print(f'Banned {len(bans)} people')
157 151
 		return bans
158 152
 
159 153
 class GuildContext:

+ 5
- 5
cogs/urlspamcog.py 파일 보기

@@ -53,7 +53,7 @@ class URLSpamCog(BaseCog):
53 53
 				# TODO: Info to mods
54 54
 
55 55
 	def __contains_url(self, text: str) -> bool:
56
-		p = re.compile(r'http[^\s]*')
56
+		p = re.compile(r'http(?:s)?://[^\s]+')
57 57
 		return p.search(text) is not None
58 58
 
59 59
 	def __format_timedelta(self, timespan: timedelta) -> str:
@@ -63,12 +63,12 @@ class URLSpamCog(BaseCog):
63 63
 		m = (timespan.seconds // 60) % 60
64 64
 		s = timespan.seconds % 60
65 65
 		if d > 0:
66
-			parts.append(f'{d}d')
66
+			parts.append(f'{d} days')
67 67
 		if d > 0 or h > 0:
68
-			parts.append(f'{h}h')
68
+			parts.append(f'{h} hours')
69 69
 		if d > 0 or h > 0 or m > 0:
70
-			parts.append(f'{m}m')
71
-		parts.append(f'{s}s')
70
+			parts.append(f'{m} minutes')
71
+		parts.append(f'{s} seconds')
72 72
 		# Limit the precision to the two most significant elements
73 73
 		while len(parts) > 2:
74 74
 			parts.pop(-1)

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

@@ -7,6 +7,7 @@ CONFIG = {
7 7
 	'ban_emoji': '🚫',
8 8
 	'trash_emoji': '🗑',
9 9
 	'success_emoji': '✅',
10
+	'failure_emoji': '❌',
10 11
 	'warning_emoji': '⚠️',
11 12
 	'info_emoji': 'ℹ️',
12 13
 	'config_path': 'config/',

+ 2
- 1
storage.py 파일 보기

@@ -167,4 +167,5 @@ class Storage:
167 167
 
168 168
 	@classmethod
169 169
 	def __trace(cls, message: str) -> None:
170
-		print(f'{cls.__name__}: {message}')
170
+		# print(f'{cls.__name__}: {message}')
171
+		pass

Loading…
취소
저장