Просмотр исходного кода

Moving BotMessage and CogSetting to separate files

master
Rocketsoup 4 лет назад
Родитель
Сommit
3d2a1df391
6 измененных файлов: 565 добавлений и 550 удалений
  1. 1
    1
      .gitignore
  2. 297
    0
      rocketbot/botmessage.py
  3. 10
    544
      rocketbot/cogs/basecog.py
  4. 229
    0
      rocketbot/cogsetting.py
  5. 4
    4
      rocketbot/collections.py
  6. 24
    1
      rocketbot/utils.py

+ 1
- 1
.gitignore Просмотреть файл

119
 
119
 
120
 # Rocketbot stuff
120
 # Rocketbot stuff
121
 /config.py
121
 /config.py
122
-/state/**
122
+/config/**

+ 297
- 0
rocketbot/botmessage.py Просмотреть файл

1
+"""
2
+Classes for crafting messages from the bot. Content can change as information
3
+changes, and mods can perform actions on the message via emoji reactions.
4
+"""
5
+from datetime import datetime
6
+from discord import Guild, Message, PartialEmoji, TextChannel
7
+
8
+from config import CONFIG
9
+from rocketbot.storage import ConfigKey, Storage
10
+from rocketbot.utils import bot_log
11
+
12
+class BotMessageReaction:
13
+	"""
14
+	A possible reaction to a bot message that will trigger an action. The list
15
+	of available reactions will be listed at the end of a BotMessage. When a
16
+	mod reacts to the message with the emote, something can happen.
17
+
18
+	If the reaction is disabled, reactions will not register. The description
19
+	will still show up in the message, but no emoji is shown. This can be used
20
+	to explain why an action is no longer available.
21
+	"""
22
+	def __init__(self, emoji: str, is_enabled: bool, description: str):
23
+		self.emoji = emoji
24
+		self.is_enabled = is_enabled
25
+		self.description = description
26
+
27
+	def __eq__(self, other):
28
+		return other is not None and \
29
+			other.emoji == self.emoji and \
30
+			other.is_enabled == self.is_enabled and \
31
+			other.description == self.description
32
+
33
+	@classmethod
34
+	def standard_set(cls,
35
+			did_delete: bool = None,
36
+			message_count: int = 1,
37
+			did_kick: bool = None,
38
+			did_ban: bool = None,
39
+			user_count: int = 1) -> list:
40
+		"""
41
+		Convenience factory for generating any of the three most common
42
+		commands: delete message(s), kick user(s), and ban user(s). All
43
+		arguments are optional. Resulting list can be passed directly to
44
+		`BotMessage.set_reactions()`.
45
+
46
+		Params
47
+		- did_delete     Whether the message(s) have been deleted. Pass True or
48
+		                 False if this applies, omit to leave out delete action.
49
+		- message_count  How many messages there are. Used for pluralizing
50
+		                 description. Defaults to 1. Omit if n/a.
51
+		- did_kick       Whether the user(s) have been kicked. Pass True or
52
+		                 False if this applies, omit to leave out kick action.
53
+		- did_ban        Whether the user(s) have been banned. Pass True or
54
+		                 False if this applies, omit to leave out ban action.
55
+		- user_count     How many users there are. Used for pluralizing
56
+		                 description. Defaults to 1. Omit if n/a.
57
+		"""
58
+		reactions = []
59
+		if did_delete is not None:
60
+			if did_delete:
61
+				reactions.append(BotMessageReaction(
62
+					CONFIG['trash_emoji'],
63
+					False,
64
+					'Message deleted' if message_count == 1 else 'Messages deleted'))
65
+			else:
66
+				reactions.append(BotMessageReaction(
67
+					CONFIG['trash_emoji'],
68
+					True,
69
+					'Delete message' if message_count == 1 else 'Delete messages'))
70
+		if did_kick is not None:
71
+			if did_ban is not None and did_ban:
72
+				# Don't show kick option at all if we also banned
73
+				pass
74
+			elif did_kick:
75
+				reactions.append(BotMessageReaction(
76
+					CONFIG['kick_emoji'],
77
+					False,
78
+					'User kicked' if user_count == 1 else 'Users kicked'))
79
+			else:
80
+				reactions.append(BotMessageReaction(
81
+					CONFIG['kick_emoji'],
82
+					True,
83
+					'Kick user' if user_count == 1 else 'Kick users'))
84
+		if did_ban is not None:
85
+			if did_ban:
86
+				reactions.append(BotMessageReaction(
87
+					CONFIG['ban_emoji'],
88
+					False,
89
+					'User banned' if user_count == 1 else 'Users banned'))
90
+			else:
91
+				reactions.append(BotMessageReaction(
92
+					CONFIG['ban_emoji'],
93
+					True,
94
+					'Ban user' if user_count == 1 else 'Ban users'))
95
+		return reactions
96
+
97
+class BotMessage:
98
+	"""
99
+	Holds state for a bot-generated message. A message is composed, sent via
100
+	`BaseCog.post_message()`, and can later be updated.
101
+
102
+	A message consists of a type (e.g. info, warning), text, optional quoted
103
+	text (such as the content of a flagged message), and an optional list of
104
+	actions that can be taken via a mod reacting to the message.
105
+	"""
106
+
107
+	TYPE_DEFAULT = 0
108
+	TYPE_INFO = 1
109
+	TYPE_MOD_WARNING = 2
110
+	TYPE_SUCCESS = 3
111
+	TYPE_FAILURE = 4
112
+
113
+	def __init__(self,
114
+			guild: Guild,
115
+			text: str,
116
+			type: int = TYPE_DEFAULT, # pylint: disable=redefined-builtin
117
+			context = None,
118
+			reply_to: Message = None):
119
+		self.guild = guild
120
+		self.text = text
121
+		self.type = type
122
+		self.context = context
123
+		self.quote = None
124
+		self.source_cog = None  # Set by `BaseCog.post_message()`
125
+		self.__posted_text = None  # last text posted, to test for changes
126
+		self.__posted_emoji = set()
127
+		self.__message = None  # Message
128
+		self.__reply_to = reply_to
129
+		self.__reactions = []  # BotMessageReaction[]
130
+
131
+	def is_sent(self) -> bool:
132
+		"""
133
+		Returns whether this message has been sent to the guild. This may
134
+		continue returning False even after calling BaseCog.post_message if
135
+		the guild has no configured warning channel.
136
+		"""
137
+		return self.__message is not None
138
+
139
+	def message_id(self):
140
+		'Returns the Message id or None if not sent.'
141
+		return self.__message.id if self.__message else None
142
+
143
+	def message_sent_at(self) -> datetime:
144
+		'Returns when the message was sent or None if not sent.'
145
+		return self.__message.created_at if self.__message else None
146
+
147
+	def has_reactions(self) -> bool:
148
+		'Whether this message has any reactions defined.'
149
+		return len(self.__reactions) > 0
150
+
151
+	async def set_text(self, new_text: str) -> None:
152
+		"""
153
+		Replaces the text of this message. If the message has been sent, it will
154
+		be updated.
155
+		"""
156
+		self.text = new_text
157
+		await self.update_if_sent()
158
+
159
+	async def set_reactions(self, reactions: list) -> None:
160
+		"""
161
+		Replaces all BotMessageReactions with a new list. If the message has
162
+		been sent, it will be updated.
163
+		"""
164
+		if reactions == self.__reactions:
165
+			# No change
166
+			return
167
+		self.__reactions = reactions.copy() if reactions is not None else []
168
+		await self.update_if_sent()
169
+
170
+	async def add_reaction(self, reaction: BotMessageReaction) -> None:
171
+		"""
172
+		Adds one BotMessageReaction to this message. If a reaction already
173
+		exists for the given emoji it is replaced with the new one. If the
174
+		message has been sent, it will be updated.
175
+		"""
176
+		# Alias for update. Makes for clearer intent.
177
+		await self.update_reaction(reaction)
178
+
179
+	async def update_reaction(self, reaction: BotMessageReaction) -> None:
180
+		"""
181
+		Updates or adds a BotMessageReaction. If the message has been sent, it
182
+		will be updated.
183
+		"""
184
+		found = False
185
+		for i, existing in enumerate(self.__reactions):
186
+			if existing.emoji == reaction.emoji:
187
+				if reaction == self.__reactions[i]:
188
+					# No change
189
+					return
190
+				self.__reactions[i] = reaction
191
+				found = True
192
+				break
193
+		if not found:
194
+			self.__reactions.append(reaction)
195
+		await self.update_if_sent()
196
+
197
+	async def remove_reaction(self, reaction_or_emoji) -> None:
198
+		"""
199
+		Removes a reaction. Can pass either a BotMessageReaction or just the
200
+		emoji string. If the message has been sent, it will be updated.
201
+		"""
202
+		for i, existing in enumerate(self.__reactions):
203
+			if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
204
+				(isinstance(reaction_or_emoji, BotMessageReaction) and \
205
+					existing.emoji == reaction_or_emoji.emoji):
206
+				self.__reactions.pop(i)
207
+				await self.update_if_sent()
208
+				return
209
+
210
+	def reaction_for_emoji(self, emoji) -> BotMessageReaction:
211
+		"""
212
+		Finds the BotMessageReaction for the given emoji or None if not found.
213
+		Accepts either a PartialEmoji or str.
214
+		"""
215
+		for reaction in self.__reactions:
216
+			if isinstance(emoji, PartialEmoji) and reaction.emoji == emoji.name:
217
+				return reaction
218
+			if isinstance(emoji, str) and reaction.emoji == emoji:
219
+				return reaction
220
+		return None
221
+
222
+	async def update_if_sent(self) -> None:
223
+		"""
224
+		Updates the text and/or reactions on a message if it was sent to
225
+		the guild, otherwise does nothing. Does not need to be called by
226
+		BaseCog subclasses.
227
+		"""
228
+		if self.__message:
229
+			await self.update()
230
+
231
+	async def update(self) -> None:
232
+		"""
233
+		Sends or updates an already sent message based on BotMessage state.
234
+		Does not need to be called by BaseCog subclasses.
235
+		"""
236
+		content: str = self.__formatted_message()
237
+		if self.__message:
238
+			if content != self.__posted_text:
239
+				await self.__message.edit(content=content)
240
+				self.__posted_text = content
241
+		else:
242
+			if self.__reply_to:
243
+				self.__message = await self.__reply_to.reply(content=content, mention_author=False)
244
+				self.__posted_text = content
245
+			else:
246
+				channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
247
+				if channel_id is None:
248
+					bot_log(self.guild, None, '\u0007No warning channel set! No warning issued.')
249
+					return
250
+				channel: TextChannel = self.guild.get_channel(channel_id)
251
+				if channel is None:
252
+					bot_log(self.guild, None, '\u0007Configured warning channel does not exist!')
253
+					return
254
+				self.__message = await channel.send(content=content)
255
+				self.__posted_text = content
256
+		emoji_to_remove = self.__posted_emoji.copy()
257
+		for reaction in self.__reactions:
258
+			if reaction.is_enabled:
259
+				if reaction.emoji not in self.__posted_emoji:
260
+					await self.__message.add_reaction(reaction.emoji)
261
+					self.__posted_emoji.add(reaction.emoji)
262
+				if reaction.emoji in emoji_to_remove:
263
+					emoji_to_remove.remove(reaction.emoji)
264
+		for emoji in emoji_to_remove:
265
+			await self.__message.clear_reaction(emoji)
266
+			if emoji in self.__posted_emoji:
267
+				self.__posted_emoji.remove(emoji)
268
+
269
+	def __formatted_message(self) -> str:
270
+		s: str = ''
271
+
272
+		if self.type == self.TYPE_INFO:
273
+			s += CONFIG['info_emoji'] + ' '
274
+		elif self.type == self.TYPE_MOD_WARNING:
275
+			mention: str = Storage.get_config_value(self.guild, ConfigKey.WARNING_MENTION)
276
+			if mention:
277
+				s += mention + ' '
278
+			s += CONFIG['warning_emoji'] + ' '
279
+		elif self.type == self.TYPE_SUCCESS:
280
+			s += CONFIG['success_emoji'] + ' '
281
+		elif self.type == self.TYPE_FAILURE:
282
+			s += CONFIG['failure_emoji'] + ' '
283
+
284
+		s += self.text
285
+
286
+		if self.quote:
287
+			s += f'\n\n> {self.quote}'
288
+
289
+		if len(self.__reactions) > 0:
290
+			s += '\n\nAvailable actions:'
291
+			for reaction in self.__reactions:
292
+				if reaction.is_enabled:
293
+					s += f'\n     {reaction.emoji} {reaction.description}'
294
+				else:
295
+					s += f'\n     {reaction.description}'
296
+
297
+		return s

+ 10
- 544
rocketbot/cogs/basecog.py Просмотреть файл

2
 Base cog class and helper classes.
2
 Base cog class and helper classes.
3
 """
3
 """
4
 from datetime import datetime, timedelta
4
 from datetime import datetime, timedelta
5
-from discord import Guild, Member, Message, PartialEmoji, RawReactionActionEvent, TextChannel
5
+from discord import Guild, Member, Message, RawReactionActionEvent
6
 from discord.abc import GuildChannel
6
 from discord.abc import GuildChannel
7
 from discord.ext import commands
7
 from discord.ext import commands
8
 
8
 
9
 from config import CONFIG
9
 from config import CONFIG
10
+from rocketbot.botmessage import BotMessage, BotMessageReaction
11
+from rocketbot.cogsetting import CogSetting
10
 from rocketbot.collections import AgeBoundDict
12
 from rocketbot.collections import AgeBoundDict
11
-from rocketbot.storage import ConfigKey, Storage
12
-
13
-class BotMessageReaction:
14
-	"""
15
-	A possible reaction to a bot message that will trigger an action. The list
16
-	of available reactions will be listed at the end of a BotMessage. When a
17
-	mod reacts to the message with the emote, something can happen.
18
-
19
-	If the reaction is disabled, reactions will not register. The description
20
-	will still show up in the message, but no emoji is shown. This can be used
21
-	to explain why an action is no longer available.
22
-	"""
23
-	def __init__(self, emoji: str, is_enabled: bool, description: str):
24
-		self.emoji = emoji
25
-		self.is_enabled = is_enabled
26
-		self.description = description
27
-
28
-	def __eq__(self, other):
29
-		return other is not None and \
30
-			other.emoji == self.emoji and \
31
-			other.is_enabled == self.is_enabled and \
32
-			other.description == self.description
33
-
34
-	@classmethod
35
-	def standard_set(cls,
36
-			did_delete: bool = None,
37
-			message_count: int = 1,
38
-			did_kick: bool = None,
39
-			did_ban: bool = None,
40
-			user_count: int = 1) -> list:
41
-		"""
42
-		Convenience factory for generating any of the three most common
43
-		commands: delete message(s), kick user(s), and ban user(s). All
44
-		arguments are optional. Resulting list can be passed directly to
45
-		`BotMessage.set_reactions()`.
46
-
47
-		Params
48
-		- did_delete     Whether the message(s) have been deleted. Pass True or
49
-		                 False if this applies, omit to leave out delete action.
50
-		- message_count  How many messages there are. Used for pluralizing
51
-		                 description. Defaults to 1. Omit if n/a.
52
-		- did_kick       Whether the user(s) have been kicked. Pass True or
53
-		                 False if this applies, omit to leave out kick action.
54
-		- did_ban        Whether the user(s) have been banned. Pass True or
55
-		                 False if this applies, omit to leave out ban action.
56
-		- user_count     How many users there are. Used for pluralizing
57
-		                 description. Defaults to 1. Omit if n/a.
58
-		"""
59
-		reactions = []
60
-		if did_delete is not None:
61
-			if did_delete:
62
-				reactions.append(BotMessageReaction(
63
-					CONFIG['trash_emoji'],
64
-					False,
65
-					'Message deleted' if message_count == 1 else 'Messages deleted'))
66
-			else:
67
-				reactions.append(BotMessageReaction(
68
-					CONFIG['trash_emoji'],
69
-					True,
70
-					'Delete message' if message_count == 1 else 'Delete messages'))
71
-		if did_kick is not None:
72
-			if did_ban is not None and did_ban:
73
-				# Don't show kick option at all if we also banned
74
-				pass
75
-			elif did_kick:
76
-				reactions.append(BotMessageReaction(
77
-					CONFIG['kick_emoji'],
78
-					False,
79
-					'User kicked' if user_count == 1 else 'Users kicked'))
80
-			else:
81
-				reactions.append(BotMessageReaction(
82
-					CONFIG['kick_emoji'],
83
-					True,
84
-					'Kick user' if user_count == 1 else 'Kick users'))
85
-		if did_ban is not None:
86
-			if did_ban:
87
-				reactions.append(BotMessageReaction(
88
-					CONFIG['ban_emoji'],
89
-					False,
90
-					'User banned' if user_count == 1 else 'Users banned'))
91
-			else:
92
-				reactions.append(BotMessageReaction(
93
-					CONFIG['ban_emoji'],
94
-					True,
95
-					'Ban user' if user_count == 1 else 'Ban users'))
96
-		return reactions
97
-
98
-class BotMessage:
99
-	"""
100
-	Holds state for a bot-generated message. A message is composed, sent via
101
-	`BaseCog.post_message()`, and can later be updated.
102
-
103
-	A message consists of a type (e.g. info, warning), text, optional quoted
104
-	text (such as the content of a flagged message), and an optional list of
105
-	actions that can be taken via a mod reacting to the message.
106
-	"""
107
-
108
-	TYPE_DEFAULT = 0
109
-	TYPE_INFO = 1
110
-	TYPE_MOD_WARNING = 2
111
-	TYPE_SUCCESS = 3
112
-	TYPE_FAILURE = 4
113
-
114
-	def __init__(self,
115
-			guild: Guild,
116
-			text: str,
117
-			type: int = TYPE_DEFAULT,
118
-			context = None,
119
-			reply_to: Message = None):
120
-		self.guild = guild
121
-		self.text = text
122
-		self.type = type
123
-		self.context = context
124
-		self.quote = None
125
-		self.source_cog = None  # Set by `BaseCog.post_message()`
126
-		self.__posted_text = None  # last text posted, to test for changes
127
-		self.__posted_emoji = set()
128
-		self.__message = None  # Message
129
-		self.__reply_to = reply_to
130
-		self.__reactions = []  # BotMessageReaction[]
131
-
132
-	def is_sent(self) -> bool:
133
-		"""
134
-		Returns whether this message has been sent to the guild. This may
135
-		continue returning False even after calling BaseCog.post_message if
136
-		the guild has no configured warning channel.
137
-		"""
138
-		return self.__message is not None
139
-
140
-	def message_id(self):
141
-		'Returns the Message id or None if not sent.'
142
-		return self.__message.id if self.__message else None
143
-
144
-	def message_sent_at(self) -> datetime:
145
-		'Returns when the message was sent or None if not sent.'
146
-		return self.__message.created_at if self.__message else None
147
-
148
-	def has_reactions(self) -> bool:
149
-		'Whether this message has any reactions defined.'
150
-		return len(self.__reactions) > 0
151
-
152
-	async def set_text(self, new_text: str) -> None:
153
-		"""
154
-		Replaces the text of this message. If the message has been sent, it will
155
-		be updated.
156
-		"""
157
-		self.text = new_text
158
-		await self.update_if_sent()
159
-
160
-	async def set_reactions(self, reactions: list) -> None:
161
-		"""
162
-		Replaces all BotMessageReactions with a new list. If the message has
163
-		been sent, it will be updated.
164
-		"""
165
-		if reactions == self.__reactions:
166
-			# No change
167
-			return
168
-		self.__reactions = reactions.copy() if reactions is not None else []
169
-		await self.update_if_sent()
170
-
171
-	async def add_reaction(self, reaction: BotMessageReaction) -> None:
172
-		"""
173
-		Adds one BotMessageReaction to this message. If a reaction already
174
-		exists for the given emoji it is replaced with the new one. If the
175
-		message has been sent, it will be updated.
176
-		"""
177
-		# Alias for update. Makes for clearer intent.
178
-		await self.update_reaction(reaction)
179
-
180
-	async def update_reaction(self, reaction: BotMessageReaction) -> None:
181
-		"""
182
-		Updates or adds a BotMessageReaction. If the message has been sent, it
183
-		will be updated.
184
-		"""
185
-		found = False
186
-		for i, existing in enumerate(self.__reactions):
187
-			if existing.emoji == reaction.emoji:
188
-				if reaction == self.__reactions[i]:
189
-					# No change
190
-					return
191
-				self.__reactions[i] = reaction
192
-				found = True
193
-				break
194
-		if not found:
195
-			self.__reactions.append(reaction)
196
-		await self.update_if_sent()
197
-
198
-	async def remove_reaction(self, reaction_or_emoji) -> None:
199
-		"""
200
-		Removes a reaction. Can pass either a BotMessageReaction or just the
201
-		emoji string. If the message has been sent, it will be updated.
202
-		"""
203
-		for i, existing in enumerate(self.__reactions):
204
-			if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
205
-				(isinstance(reaction_or_emoji, BotMessageReaction) and \
206
-					existing.emoji == reaction_or_emoji.emoji):
207
-				self.__reactions.pop(i)
208
-				await self.update_if_sent()
209
-				return
210
-
211
-	def reaction_for_emoji(self, emoji) -> BotMessageReaction:
212
-		"""
213
-		Finds the BotMessageReaction for the given emoji or None if not found.
214
-		Accepts either a PartialEmoji or str.
215
-		"""
216
-		for reaction in self.__reactions:
217
-			if isinstance(emoji, PartialEmoji) and reaction.emoji == emoji.name:
218
-				return reaction
219
-			if isinstance(emoji, str) and reaction.emoji == emoji:
220
-				return reaction
221
-		return None
222
-
223
-	async def update_if_sent(self) -> None:
224
-		"""
225
-		Updates the text and/or reactions on a message if it was sent to
226
-		the guild, otherwise does nothing. Does not need to be called by
227
-		BaseCog subclasses.
228
-		"""
229
-		if self.__message:
230
-			await self.update()
231
-
232
-	async def update(self) -> None:
233
-		"""
234
-		Sends or updates an already sent message based on BotMessage state.
235
-		Does not need to be called by BaseCog subclasses.
236
-		"""
237
-		content: str = self.__formatted_message()
238
-		if self.__message:
239
-			if content != self.__posted_text:
240
-				await self.__message.edit(content=content)
241
-				self.__posted_text = content
242
-		else:
243
-			if self.__reply_to:
244
-				self.__message = await self.__reply_to.reply(content=content, mention_author=False)
245
-				self.__posted_text = content
246
-			else:
247
-				channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
248
-				if channel_id is None:
249
-					BaseCog.log(self.guild, '\u0007No warning channel set! No warning issued.')
250
-					return
251
-				channel: TextChannel = self.guild.get_channel(channel_id)
252
-				if channel is None:
253
-					BaseCog.log(self.guild, '\u0007Configured warning channel does not exist!')
254
-					return
255
-				self.__message = await channel.send(content=content)
256
-				self.__posted_text = content
257
-		emoji_to_remove = self.__posted_emoji.copy()
258
-		for reaction in self.__reactions:
259
-			if reaction.is_enabled:
260
-				if reaction.emoji not in self.__posted_emoji:
261
-					await self.__message.add_reaction(reaction.emoji)
262
-					self.__posted_emoji.add(reaction.emoji)
263
-				if reaction.emoji in emoji_to_remove:
264
-					emoji_to_remove.remove(reaction.emoji)
265
-		for emoji in emoji_to_remove:
266
-			await self.__message.clear_reaction(emoji)
267
-			if emoji in self.__posted_emoji:
268
-				self.__posted_emoji.remove(emoji)
269
-
270
-	def __formatted_message(self) -> str:
271
-		s: str = ''
272
-
273
-		if self.type == self.TYPE_INFO:
274
-			s += CONFIG['info_emoji'] + ' '
275
-		elif self.type == self.TYPE_MOD_WARNING:
276
-			mention: str = Storage.get_config_value(self.guild, ConfigKey.WARNING_MENTION)
277
-			if mention:
278
-				s += mention + ' '
279
-			s += CONFIG['warning_emoji'] + ' '
280
-		elif self.type == self.TYPE_SUCCESS:
281
-			s += CONFIG['success_emoji'] + ' '
282
-		elif self.type == self.TYPE_FAILURE:
283
-			s += CONFIG['failure_emoji'] + ' '
284
-
285
-		s += self.text
286
-
287
-		if self.quote:
288
-			s += f'\n\n> {self.quote}'
289
-
290
-		if len(self.__reactions) > 0:
291
-			s += '\n\nAvailable actions:'
292
-			for reaction in self.__reactions:
293
-				if reaction.is_enabled:
294
-					s += f'\n     {reaction.emoji} {reaction.description}'
295
-				else:
296
-					s += f'\n     {reaction.description}'
297
-
298
-		return s
299
-
300
-class CogSetting:
301
-	"""
302
-	Describes a configuration setting for a guild that can be edited by the
303
-	mods of those guilds. BaseCog can generate "get" and "set" commands
304
-	automatically, reducing the boilerplate of generating commands manually.
305
-	Offers simple validation rules.
306
-	"""
307
-	def __init__(self,
308
-			name: str,
309
-			datatype,
310
-			brief: str = None,
311
-			description: str = None,
312
-			usage: str = None,
313
-			min_value = None,
314
-			max_value = None,
315
-			enum_values: set = None):
316
-		"""
317
-		Params:
318
-		- name         Setting identifier. Must follow variable naming
319
-		               conventions.
320
-		- datatype     Datatype of the setting. E.g. int, float, str
321
-		- brief        Description of the setting, starting with lower case.
322
-		               Will be inserted into phrases like "Sets <brief>" and
323
-					   "Gets <brief".
324
-		- description  Long-form description. Min, max, and enum values will be
325
-		               appended to the end, so does not need to include these.
326
-		- usage        Description of the value argument in a set command, e.g.
327
-		               "<maxcount:int>"
328
-		- min_value    Smallest allowable value. Must be of the same datatype as
329
-		               the value. None for no minimum.
330
-		- max_value    Largest allowable value. None for no maximum.
331
-		- enum_values  Set of allowed values. None if unconstrained.
332
-		"""
333
-		self.name = name
334
-		self.datatype = datatype
335
-		self.brief = brief
336
-		self.description = description or ''  # Can't be None
337
-		self.usage = usage
338
-		self.min_value = min_value
339
-		self.max_value = max_value
340
-		self.enum_values = enum_values
341
-		if self.enum_values or self.min_value is not None or self.max_value is not None:
342
-			self.description += '\n'
343
-		if self.enum_values:
344
-			allowed_values = '`' + ('`, `'.join(enum_values)) + '`'
345
-			self.description += f'\nAllowed values: {allowed_values}'
346
-		if self.min_value is not None:
347
-			self.description += f'\nMin value: {self.min_value}'
348
-		if self.max_value is not None:
349
-			self.description += f'\nMax value: {self.max_value}'
350
-		if self.usage is None:
351
-			self.usage = f'<{self.name}>'
13
+from rocketbot.storage import Storage
14
+from rocketbot.utils import bot_log
352
 
15
 
353
 class BaseCog(commands.Cog):
16
 class BaseCog(commands.Cog):
354
 	"""
17
 	"""
419
 		will be raised if the new value does not pass validation specified in
82
 		will be raised if the new value does not pass validation specified in
420
 		the CogSetting.
83
 		the CogSetting.
421
 		"""
84
 		"""
422
-		if setting.min_value is not None and new_value < setting.min_value:
423
-			raise ValueError(f'{setting.name} must be at least {setting.min_value}')
424
-		if setting.max_value is not None and new_value > setting.max_value:
425
-			raise ValueError(f'{setting.name} must be no more than {setting.max_value}')
426
-		if setting.enum_values and new_value not in setting.enum_values:
427
-			raise ValueError(f'{setting.name} must be one of {setting.enum_values}')
85
+		setting.validate_value(new_value)
428
 		key = f'{cls.__name__}.{setting.name}'
86
 		key = f'{cls.__name__}.{setting.name}'
429
 		Storage.set_config_value(guild, key, new_value)
87
 		Storage.set_config_value(guild, key, new_value)
430
 
88
 
431
 	@commands.Cog.listener()
89
 	@commands.Cog.listener()
432
 	async def on_ready(self):
90
 	async def on_ready(self):
433
 		'Event listener'
91
 		'Event listener'
434
-		self.__set_up_setting_commands()
435
-
436
-	def __set_up_setting_commands(self):
437
-		"""
438
-		Sets up commands for editing all registered cog settings. This method
439
-		only runs once.
440
-		"""
441
-		if self.are_settings_setup:
442
-			return
443
-		self.are_settings_setup = True
444
-
445
-		# See if the cog has a command group. Currently only supporting one max.
446
-		group: commands.core.Group = None
447
-		for member_name in dir(self):
448
-			member = getattr(self, member_name)
449
-			if isinstance(member, commands.core.Group):
450
-				group = member
451
-				break
452
-
453
-		for setting in self.settings:
454
-			if setting.name == 'enabled' or setting.name == 'is_enabled':
455
-				self.__make_enable_disable_commands(setting, group)
456
-			else:
457
-				self.__make_getter_setter_commands(setting, group)
458
-
459
-	def __make_getter_setter_commands(self,
460
-			setting: CogSetting,
461
-			group: commands.core.Group) -> None:
462
-		"""
463
-		Creates a "get..." and "set..." command for the given setting and
464
-		either registers them as subcommands under the given command group or
465
-		under the bot if `None`.
466
-		"""
467
-		# Manually constructing equivalent of:
468
-		# 	@commands.command()
469
-		# 	@commands.has_permissions(ban_members=True)
470
-		# 	@commands.guild_only()
471
-		# 	async def getvar(self, context):
472
-		async def getter(self, context):
473
-			await self.__get_setting_command(context, setting)
474
-		async def setter_int(self, context, new_value: int):
475
-			await self.__set_setting_command(context, new_value, setting)
476
-		async def setter_float(self, context, new_value: float):
477
-			await self.__set_setting_command(context, new_value, setting)
478
-		async def setter_str(self, context, new_value: str):
479
-			await self.__set_setting_command(context, new_value, setting)
480
-		async def setter_bool(self, context, new_value: bool):
481
-			await self.__set_setting_command(context, new_value, setting)
482
-
483
-		setter = None
484
-		if setting.datatype == int:
485
-			setter = setter_int
486
-		elif setting.datatype == float:
487
-			setter = setter_float
488
-		elif setting.datatype == str:
489
-			setter = setter_str
490
-		elif setting.datatype == bool:
491
-			setter = setter_bool
492
-		else:
493
-			raise RuntimeError(f'Datatype {setting.datatype} unsupported')
494
-
495
-		get_command = commands.Command(
496
-			getter,
497
-			name=f'get{setting.name}',
498
-			brief=f'Shows {setting.brief}',
499
-			description=setting.description,
500
-			checks=[
501
-				commands.has_permissions(ban_members=True),
502
-				commands.guild_only(),
503
-			])
504
-		set_command = commands.Command(
505
-			setter,
506
-			name=f'set{setting.name}',
507
-			brief=f'Sets {setting.brief}',
508
-			description=setting.description,
509
-			usage=setting.usage,
510
-			checks=[
511
-				commands.has_permissions(ban_members=True),
512
-				commands.guild_only(),
513
-			])
514
-
515
-		# Passing `cog` in init gets ignored and set to `None` so set after.
516
-		# This ensures the callback is passed `self`.
517
-		get_command.cog = self
518
-		set_command.cog = self
519
-
520
-		if group:
521
-			group.add_command(get_command)
522
-			group.add_command(set_command)
523
-		else:
524
-			self.bot.add_command(get_command)
525
-			self.bot.add_command(set_command)
526
-
527
-	def __make_enable_disable_commands(self,
528
-			setting: CogSetting,
529
-			group: commands.core.Group) -> None:
530
-		"""
531
-		Creates "enable" and "disable" commands.
532
-		"""
533
-		async def enabler(self, context):
534
-			await self.__enable_command(context, setting)
535
-		async def disabler(self, context):
536
-			await self.__disable_command(context, setting)
537
-
538
-		enable_command = commands.Command(
539
-			enabler,
540
-			name='enable',
541
-			brief=f'Enables {setting.brief}',
542
-			description=setting.description,
543
-			checks=[
544
-				commands.has_permissions(ban_members=True),
545
-				commands.guild_only(),
546
-			])
547
-		disable_command = commands.Command(
548
-			disabler,
549
-			name='disable',
550
-			brief=f'Disables {setting.brief}',
551
-			description=setting.description,
552
-			checks=[
553
-				commands.has_permissions(ban_members=True),
554
-				commands.guild_only(),
555
-			])
556
-
557
-		enable_command.cog = self
558
-		disable_command.cog = self
559
-
560
-		if group:
561
-			group.add_command(enable_command)
562
-			group.add_command(disable_command)
563
-		else:
564
-			self.bot.add_command(enable_command)
565
-			self.bot.add_command(disable_command)
566
-
567
-	async def __set_setting_command(self, context, new_value, setting) -> None:
568
-		setting_name = setting.name
569
-		if context.command.parent:
570
-			setting_name = f'{context.command.parent.name}.{setting_name}'
571
-		if setting.min_value is not None and new_value < setting.min_value:
572
-			await context.message.reply(
573
-				f'{CONFIG["failure_emoji"]} `{setting_name}` must be >= {setting.min_value}',
574
-				mention_author=False)
575
-			return
576
-		if setting.max_value is not None and new_value > setting.max_value:
577
-			await context.message.reply(
578
-				f'{CONFIG["failure_emoji"]} `{setting_name}` must be <= {setting.max_value}',
579
-				mention_author=False)
580
-			return
581
-		if setting.enum_values is not None and new_value not in setting.enum_values:
582
-			allowed_values = '`' + ('`, `'.join(setting.enum_values)) + '`'
583
-			await context.message.reply(
584
-				f'{CONFIG["failure_emoji"]} `{setting_name}` must be one of {allowed_values}',
585
-				mention_author=False)
586
-			return
587
-		key = f'{self.__class__.__name__}.{setting.name}'
588
-		Storage.set_config_value(context.guild, key, new_value)
589
-		await context.message.reply(
590
-			f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
591
-			mention_author=False)
592
-		await self.on_setting_updated(context.guild, setting)
593
-		self.log(context.guild, f'{context.author.name} set {key} to {new_value}')
594
-
595
-	async def __get_setting_command(self, context, setting) -> None:
596
-		setting_name = setting.name
597
-		if context.command.parent:
598
-			setting_name = f'{context.command.parent.name}.{setting_name}'
599
-		key = f'{self.__class__.__name__}.{setting.name}'
600
-		value = Storage.get_config_value(context.guild, key)
601
-		if value is None:
602
-			value = self.get_cog_default(setting.name)
603
-			await context.message.reply(
604
-				f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
605
-				mention_author=False)
606
-		else:
607
-			await context.message.reply(
608
-				f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
609
-				mention_author=False)
610
-
611
-	async def __enable_command(self, context, setting) -> None:
612
-		key = f'{self.__class__.__name__}.{setting.name}'
613
-		Storage.set_config_value(context.guild, key, True)
614
-		await context.message.reply(
615
-			f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
616
-			mention_author=False)
617
-		await self.on_setting_updated(context.guild, setting)
618
-		self.log(context.guild, f'{context.author.name} enabled {self.__class__.__name__}')
619
-
620
-	async def __disable_command(self, context, setting) -> None:
621
-		key = f'{self.__class__.__name__}.{setting.name}'
622
-		Storage.set_config_value(context.guild, key, False)
623
-		await context.message.reply(
624
-			f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
625
-			mention_author=False)
626
-		await self.on_setting_updated(context.guild, setting)
627
-		self.log(context.guild, f'{context.author.name} disabled {self.__class__.__name__}')
92
+		if not self.are_settings_setup:
93
+			self.are_settings_setup = True
94
+			CogSetting.set_up_all(self, self.bot, self.settings)
628
 
95
 
629
 	async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
96
 	async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
630
 		"""
97
 		"""
713
 		"""
180
 		"""
714
 		Writes a message to the console. Intended for significant events only.
181
 		Writes a message to the console. Intended for significant events only.
715
 		"""
182
 		"""
716
-		now = datetime.now()
717
-		print(f'[{now.strftime("%Y-%m-%dT%H:%M:%S")}|{cls.__name__}|{guild.name}] {message}')
183
+		bot_log(guild, cls, message)

+ 229
- 0
rocketbot/cogsetting.py Просмотреть файл

1
+"""
2
+A guild configuration setting available for editing via bot commands.
3
+"""
4
+from discord.ext import commands
5
+from discord.ext.commands import Bot, Cog, Command, Context, Group
6
+
7
+from config import CONFIG
8
+from rocketbot.storage import Storage
9
+from rocketbot.utils import first_command_group
10
+
11
+class CogSetting:
12
+	"""
13
+	Describes a configuration setting for a guild that can be edited by the
14
+	mods of those guilds. BaseCog can generate "get" and "set" commands
15
+	automatically, reducing the boilerplate of generating commands manually.
16
+	Offers simple validation rules.
17
+	"""
18
+	def __init__(self,
19
+			name: str,
20
+			datatype,
21
+			brief: str = None,
22
+			description: str = None,
23
+			usage: str = None,
24
+			min_value = None,
25
+			max_value = None,
26
+			enum_values: set = None):
27
+		"""
28
+		Params:
29
+		- name         Setting identifier. Must follow variable naming
30
+		               conventions.
31
+		- datatype     Datatype of the setting. E.g. int, float, str
32
+		- brief        Description of the setting, starting with lower case.
33
+		               Will be inserted into phrases like "Sets <brief>" and
34
+					   "Gets <brief".
35
+		- description  Long-form description. Min, max, and enum values will be
36
+		               appended to the end, so does not need to include these.
37
+		- usage        Description of the value argument in a set command, e.g.
38
+		               "<maxcount:int>"
39
+		- min_value    Smallest allowable value. Must be of the same datatype as
40
+		               the value. None for no minimum.
41
+		- max_value    Largest allowable value. None for no maximum.
42
+		- enum_values  Set of allowed values. None if unconstrained.
43
+		"""
44
+		self.name = name
45
+		self.datatype = datatype
46
+		self.brief = brief
47
+		self.description = description or ''  # Can't be None
48
+		self.usage = usage
49
+		self.min_value = min_value
50
+		self.max_value = max_value
51
+		self.enum_values = enum_values
52
+		if self.enum_values or self.min_value is not None or self.max_value is not None:
53
+			self.description += '\n'
54
+		if self.enum_values:
55
+			allowed_values = '`' + ('`, `'.join(enum_values)) + '`'
56
+			self.description += f'\nAllowed values: {allowed_values}'
57
+		if self.min_value is not None:
58
+			self.description += f'\nMin value: {self.min_value}'
59
+		if self.max_value is not None:
60
+			self.description += f'\nMax value: {self.max_value}'
61
+		if self.usage is None:
62
+			self.usage = f'<{self.name}>'
63
+
64
+	def validate_value(self, new_value) -> None:
65
+		"""
66
+		Checks if a value is legal for this setting. Raises a ValueError if not.
67
+		"""
68
+		if self.min_value is not None and new_value < self.min_value:
69
+			raise ValueError(f'`{self.name}` must be >= {self.min_value}')
70
+		if self.max_value is not None and new_value > self.max_value:
71
+			raise ValueError(f'`{self.name}` must be <= {self.max_value}')
72
+		if self.enum_values is not None and new_value not in self.enum_values:
73
+			allowed_values = '`' + ('`, `'.join(self.enum_values)) + '`'
74
+			raise ValueError(f'`{self.name}` must be one of {allowed_values}')
75
+
76
+	def set_up(self, cog: Cog, bot: Bot, group: Group) -> None:
77
+		"""
78
+		Sets up getter and setter commands for this setting. This should
79
+		usually only be called by BaseCog.
80
+		"""
81
+		if self.name in ('enabled', 'is_enabled'):
82
+			(group or bot).add_command(self.__make_enable_command(cog))
83
+			(group or bot).add_command(self.__make_disable_command(cog))
84
+		else:
85
+			(group or bot).add_command(self.__make_getter_command(cog))
86
+			(group or bot).add_command(self.__make_setter_command(cog))
87
+
88
+	def __make_getter_command(self, cog: Cog) -> Command:
89
+		setting = self
90
+		async def getter(cog: Cog, context: Context) -> None:
91
+			setting_name = setting.name
92
+			if context.command.parent:
93
+				setting_name = f'{context.command.parent.name}.{setting_name}'
94
+			key = f'{cog.__class__.__name__}.{setting.name}'
95
+			value = Storage.get_config_value(context.guild, key)
96
+			if value is None:
97
+				value = cog.get_cog_default(setting.name)
98
+				await context.message.reply(
99
+					f'{CONFIG["info_emoji"]} `{setting_name}` is using default of `{value}`',
100
+					mention_author=False)
101
+			else:
102
+				await context.message.reply(
103
+					f'{CONFIG["info_emoji"]} `{setting_name}` is set to `{value}`',
104
+					mention_author=False)
105
+		command = Command(
106
+			getter,
107
+			name=f'get{setting.name}',
108
+			brief=f'Shows {setting.brief}',
109
+			description=setting.description,
110
+			checks=[
111
+				commands.has_permissions(ban_members=True),
112
+				commands.guild_only(),
113
+			])
114
+		command.cog = cog
115
+		return command
116
+
117
+	def __make_setter_command(self, cog: Cog) -> Command:
118
+		setting: CogSetting = self
119
+		async def setter_common(cog: Cog, context: Context, new_value) -> None:
120
+			try:
121
+				setting.validate_value(new_value)
122
+			except ValueError as ve:
123
+				await context.message.reply(
124
+					f'{CONFIG["failure_emoji"]} {ve}',
125
+					mention_author=False)
126
+				return
127
+			setting_name = setting.name
128
+			if context.command.parent:
129
+				setting_name = f'{context.command.parent.name}.{setting_name}'
130
+			key = f'{cog.__class__.__name__}.{setting.name}'
131
+			Storage.set_config_value(context.guild, key, new_value)
132
+			await context.message.reply(
133
+				f'{CONFIG["success_emoji"]} `{setting_name}` is now set to `{new_value}`',
134
+				mention_author=False)
135
+			await cog.on_setting_updated(context.guild, setting)
136
+			cog.log(context.guild, f'{context.author.name} set {key} to {new_value}')
137
+
138
+		async def setter_int(cog, context, new_value: int):
139
+			await setter_common(cog, context, new_value)
140
+		async def setter_float(cog, context, new_value: float):
141
+			await setter_common(cog, context, new_value)
142
+		async def setter_str(cog, context, new_value: str):
143
+			await setter_common(cog, context, new_value)
144
+		async def setter_bool(cog, context, new_value: bool):
145
+			await setter_common(cog, context, new_value)
146
+
147
+		setter = None
148
+		if setting.datatype == int:
149
+			setter = setter_int
150
+		elif setting.datatype == float:
151
+			setter = setter_float
152
+		elif setting.datatype == str:
153
+			setter = setter_str
154
+		elif setting.datatype == bool:
155
+			setter = setter_bool
156
+		else:
157
+			raise ValueError(f'Datatype {setting.datatype} unsupported')
158
+
159
+		command = Command(
160
+			setter,
161
+			name=f'set{setting.name}',
162
+			brief=f'Sets {setting.brief}',
163
+			description=setting.description,
164
+			usage=setting.usage,
165
+			checks=[
166
+				commands.has_permissions(ban_members=True),
167
+				commands.guild_only(),
168
+			])
169
+		# Passing `cog` in init gets ignored and set to `None` so set after.
170
+		# This ensures the callback is passed `self`.
171
+		command.cog = cog
172
+		return command
173
+
174
+	def __make_enable_command(self, cog: Cog) -> Command:
175
+		setting: CogSetting = self
176
+		async def enabler(cog: Cog, context: Context) -> None:
177
+			key = f'{cog.__class__.__name__}.{setting.name}'
178
+			Storage.set_config_value(context.guild, key, True)
179
+			await context.message.reply(
180
+				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} enabled.',
181
+				mention_author=False)
182
+			await cog.on_setting_updated(context.guild, setting)
183
+			cog.log(context.guild, f'{context.author.name} enabled {cog.__class__.__name__}')
184
+
185
+		command = Command(
186
+			enabler,
187
+			name='enable',
188
+			brief=f'Enables {setting.brief}',
189
+			description=setting.description,
190
+			checks=[
191
+				commands.has_permissions(ban_members=True),
192
+				commands.guild_only(),
193
+			])
194
+		command.cog = cog
195
+		return command
196
+
197
+	def __make_disable_command(self, cog: Cog) -> Command:
198
+		setting: CogSetting = self
199
+		async def disabler(cog: Cog, context: Context) -> None:
200
+			key = f'{cog.__class__.__name__}.{setting.name}'
201
+			Storage.set_config_value(context.guild, key, False)
202
+			await context.message.reply(
203
+				f'{CONFIG["success_emoji"]} {setting.brief.capitalize()} disabled.',
204
+				mention_author=False)
205
+			await cog.on_setting_updated(context.guild, setting)
206
+			cog.log(context.guild, f'{context.author.name} disabled {cog.__class__.__name__}')
207
+
208
+		command = Command(
209
+			disabler,
210
+			name='disable',
211
+			brief=f'Disables {setting.brief}',
212
+			description=setting.description,
213
+			checks=[
214
+				commands.has_permissions(ban_members=True),
215
+				commands.guild_only(),
216
+			])
217
+		command.cog = cog
218
+		return command
219
+
220
+	@classmethod
221
+	def set_up_all(cls, cog: Cog, bot: Bot, settings: list) -> None:
222
+		"""
223
+		Sets up editing commands for a list of CogSettings and adds them to a
224
+		cog. If the cog has a command Group, commands will be added to it.
225
+		Otherwise they will be added at the top level.
226
+		"""
227
+		group: Group = first_command_group(cog)
228
+		for setting in settings:
229
+			setting.set_up(cog, bot, group)

+ 4
- 4
rocketbot/collections.py Просмотреть файл

2
 Subclasses of list, set, and dict with special behaviors.
2
 Subclasses of list, set, and dict with special behaviors.
3
 """
3
 """
4
 
4
 
5
-from abc import ABC, abstractmethod
5
+from abc import ABCMeta, abstractmethod
6
 
6
 
7
 # Abstract collections
7
 # Abstract collections
8
 
8
 
9
-class AbstractMutableList(list, ABC):
9
+class AbstractMutableList(list, metaclass=ABCMeta):
10
 	"""
10
 	"""
11
 	Abstract list with hooks for custom logic before and after mutation
11
 	Abstract list with hooks for custom logic before and after mutation
12
 	operations.
12
 	operations.
86
 		self.post_mutate()
86
 		self.post_mutate()
87
 		return ret_val
87
 		return ret_val
88
 
88
 
89
-class AbstractMutableSet(set, ABC):
89
+class AbstractMutableSet(set, metaclass=ABCMeta):
90
 	"""
90
 	"""
91
 	Abstract set with hooks for custom logic before and after mutation
91
 	Abstract set with hooks for custom logic before and after mutation
92
 	operations.
92
 	operations.
171
 		self.post_mutate()
171
 		self.post_mutate()
172
 		return ret_val
172
 		return ret_val
173
 
173
 
174
-class AbstractMutableDict(dict, ABC):
174
+class AbstractMutableDict(dict, metaclass=ABCMeta):
175
 	"""
175
 	"""
176
 	Abstract dict with hooks for custom logic before and after mutation
176
 	Abstract dict with hooks for custom logic before and after mutation
177
 	operations.
177
 	operations.

+ 24
- 1
rocketbot/utils.py Просмотреть файл

2
 General utility functions.
2
 General utility functions.
3
 """
3
 """
4
 import re
4
 import re
5
-from datetime import timedelta
5
+from datetime import datetime, timedelta
6
+from discord import Guild
7
+from discord.ext.commands import Cog, Group
6
 
8
 
7
 def parse_timedelta(s: str) -> timedelta:
9
 def parse_timedelta(s: str) -> timedelta:
8
 	"""
10
 	"""
54
 	if len(components) > max_components:
56
 	if len(components) > max_components:
55
 		components = components[0:max_components]
57
 		components = components[0:max_components]
56
 	return ' '.join(components)
58
 	return ' '.join(components)
59
+
60
+def first_command_group(cog: Cog) -> Group:
61
+	"""
62
+	Returns the first command Group found in a cog.
63
+	"""
64
+	for member_name in dir(cog):
65
+		member = getattr(cog, member_name)
66
+		if isinstance(member, Group):
67
+			return member
68
+	return None
69
+
70
+def bot_log(guild: Guild, cog_class, message: str) -> None:
71
+	"""
72
+	Logs a message to stdout with time, cog, and guild info.
73
+	"""
74
+	now = datetime.now() # local
75
+	s = f'[{now.strftime("%Y-%m-%dT%H:%M:%S")}|'
76
+	s += f'{cog_class.__name__}|' if cog_class else '-|'
77
+	s += f'{guild.name}] ' if guild else '-] '
78
+	s += message
79
+	print(s)

Загрузка…
Отмена
Сохранить