Переглянути джерело

Reorganizing files

master
Rocketsoup 4 роки тому
джерело
коміт
4c0899dde1

+ 1
- 1
README.md Переглянути файл

23
 
23
 
24
 Create a "config" subdirectory under your source folder. This is where guild-specific configuration is written as JSON files.
24
 Create a "config" subdirectory under your source folder. This is where guild-specific configuration is written as JSON files.
25
 
25
 
26
-To start, run `python3 rocketbot.py`. Then visit
26
+To start, run `python3 bot.py`. Then visit
27
 https://discord.com/oauth2/authorize?client_id=[application_id]&scope=bot&permissions=395204357318,
27
 https://discord.com/oauth2/authorize?client_id=[application_id]&scope=bot&permissions=395204357318,
28
 where [application_id] is the "application id" value on your app configuration
28
 where [application_id] is the "application id" value on your app configuration
29
 "general information" page. Once invited, test if the bot is working by typing
29
 "general information" page. Once invited, test if the bot is working by typing

rocketbot.py → bot.py Переглянути файл

10
 from discord.ext import commands
10
 from discord.ext import commands
11
 
11
 
12
 from config import CONFIG
12
 from config import CONFIG
13
-from cogs.configcog import ConfigCog
14
-from cogs.crosspostcog import CrossPostCog
15
-from cogs.generalcog import GeneralCog
16
-from cogs.joinraidcog import JoinRaidCog
17
-from cogs.patterncog import PatternCog
18
-from cogs.urlspamcog import URLSpamCog
13
+from rocketbot.cogs.configcog import ConfigCog
14
+from rocketbot.cogs.crosspostcog import CrossPostCog
15
+from rocketbot.cogs.generalcog import GeneralCog
16
+from rocketbot.cogs.joinraidcog import JoinRaidCog
17
+from rocketbot.cogs.patterncog import PatternCog
18
+from rocketbot.cogs.urlspamcog import URLSpamCog
19
 
19
 
20
 CURRENT_CONFIG_VERSION = 3
20
 CURRENT_CONFIG_VERSION = 3
21
 if (CONFIG.get('__config_version') or 0) < CURRENT_CONFIG_VERSION:
21
 if (CONFIG.get('__config_version') or 0) < CURRENT_CONFIG_VERSION:

cogs/__init__.py → rocketbot/__init__.py Переглянути файл


+ 0
- 0
rocketbot/cogs/__init__.py Переглянути файл


cogs/basecog.py → rocketbot/cogs/basecog.py Переглянути файл

1
+"""
2
+Base cog class and helper classes.
3
+"""
1
 from datetime import datetime, timedelta
4
 from datetime import datetime, timedelta
2
 from discord import Guild, Member, Message, PartialEmoji, RawReactionActionEvent, TextChannel
5
 from discord import Guild, Member, Message, PartialEmoji, RawReactionActionEvent, TextChannel
3
 from discord.abc import GuildChannel
6
 from discord.abc import GuildChannel
4
 from discord.ext import commands
7
 from discord.ext import commands
5
 
8
 
6
 from config import CONFIG
9
 from config import CONFIG
7
-from rscollections import AgeBoundDict
8
-from storage import ConfigKey, Storage
10
+from rocketbot.collections import AgeBoundDict
11
+from rocketbot.storage import ConfigKey, Storage
9
 
12
 
10
 class BotMessageReaction:
13
 class BotMessageReaction:
11
 	"""
14
 	"""
111
 	def __init__(self,
114
 	def __init__(self,
112
 			guild: Guild,
115
 			guild: Guild,
113
 			text: str,
116
 			text: str,
114
-			type: int = 0, # TYPE_DEFAULT
117
+			type: int = TYPE_DEFAULT,
115
 			context = None,
118
 			context = None,
116
 			reply_to: Message = None):
119
 			reply_to: Message = None):
117
 		self.guild = guild
120
 		self.guild = guild
143
 		return self.__message.created_at if self.__message else None
146
 		return self.__message.created_at if self.__message else None
144
 
147
 
145
 	def has_reactions(self) -> bool:
148
 	def has_reactions(self) -> bool:
149
+		'Whether this message has any reactions defined.'
146
 		return len(self.__reactions) > 0
150
 		return len(self.__reactions) > 0
147
 
151
 
148
 	async def set_text(self, new_text: str) -> None:
152
 	async def set_text(self, new_text: str) -> None:
329
 		self.name = name
333
 		self.name = name
330
 		self.datatype = datatype
334
 		self.datatype = datatype
331
 		self.brief = brief
335
 		self.brief = brief
332
-		self.description = description or ''  # XXX: Can't be None
336
+		self.description = description or ''  # Can't be None
333
 		self.usage = usage
337
 		self.usage = usage
334
 		self.min_value = min_value
338
 		self.min_value = min_value
335
 		self.max_value = max_value
339
 		self.max_value = max_value
508
 				commands.guild_only(),
512
 				commands.guild_only(),
509
 			])
513
 			])
510
 
514
 
511
-		# XXX: Passing `cog` in init gets ignored and set to `None` so set after.
515
+		# Passing `cog` in init gets ignored and set to `None` so set after.
512
 		# This ensures the callback is passed `self`.
516
 		# This ensures the callback is passed `self`.
513
 		get_command.cog = self
517
 		get_command.cog = self
514
 		set_command.cog = self
518
 		set_command.cog = self

cogs/configcog.py → rocketbot/cogs/configcog.py Переглянути файл

1
+"""
2
+Cog handling general configuration for a guild.
3
+"""
1
 from discord import Guild, TextChannel
4
 from discord import Guild, TextChannel
2
 from discord.ext import commands
5
 from discord.ext import commands
3
 
6
 
4
 from config import CONFIG
7
 from config import CONFIG
5
-from storage import ConfigKey, Storage
6
-from cogs.basecog import BaseCog
8
+from rocketbot.storage import ConfigKey, Storage
9
+from rocketbot.cogs.basecog import BaseCog
7
 
10
 
8
 class ConfigCog(BaseCog, name='Configuration'):
11
 class ConfigCog(BaseCog, name='Configuration'):
9
 	"""
12
 	"""

cogs/crosspostcog.py → rocketbot/cogs/crosspostcog.py Переглянути файл

1
+"""
2
+Cog for detecting spam messages posted in multiple channels.
3
+"""
1
 from datetime import datetime, timedelta
4
 from datetime import datetime, timedelta
2
 from discord import Member, Message
5
 from discord import Member, Message
3
 from discord.ext import commands
6
 from discord.ext import commands
4
 
7
 
5
-from cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
6
 from config import CONFIG
8
 from config import CONFIG
7
-from rscollections import AgeBoundList, SizeBoundDict
8
-from storage import Storage
9
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
10
+from rocketbot.collections import AgeBoundList, SizeBoundDict
11
+from rocketbot.storage import Storage
9
 
12
 
10
 class SpamContext:
13
 class SpamContext:
11
 	"""
14
 	"""

cogs/generalcog.py → rocketbot/cogs/generalcog.py Переглянути файл

1
+"""
2
+Cog for handling most ungrouped commands and basic behaviors.
3
+"""
1
 import re
4
 import re
2
 from datetime import datetime, timedelta
5
 from datetime import datetime, timedelta
3
 from discord import Message
6
 from discord import Message
7
+from discord.errors import DiscordException
4
 from discord.ext import commands
8
 from discord.ext import commands
5
 
9
 
6
-from cogs.basecog import BaseCog, BotMessage
7
 from config import CONFIG
10
 from config import CONFIG
8
-from rbutils import parse_timedelta, describe_timedelta
9
-from storage import ConfigKey, Storage
11
+from rocketbot.cogs.basecog import BaseCog, BotMessage
12
+from rocketbot.utils import parse_timedelta, describe_timedelta
13
+from rocketbot.storage import ConfigKey, Storage
10
 
14
 
11
 class GeneralCog(BaseCog, name='General'):
15
 class GeneralCog(BaseCog, name='General'):
12
 	"""
16
 	"""
106
 		for channel in context.guild.text_channels:
110
 		for channel in context.guild.text_channels:
107
 			try:
111
 			try:
108
 				deleted_messages += await channel.purge(limit=100, check=predicate)
112
 				deleted_messages += await channel.purge(limit=100, check=predicate)
109
-			except:
113
+			except DiscordException:
110
 				# XXX: Sloppily glossing over access errors instead of checking access
114
 				# XXX: Sloppily glossing over access errors instead of checking access
111
 				pass
115
 				pass
112
 		await context.message.reply(
116
 		await context.message.reply(

cogs/joinraidcog.py → rocketbot/cogs/joinraidcog.py Переглянути файл

1
+"""
2
+Cog for detecting large numbers of guild joins in a short period of time.
3
+"""
1
 import weakref
4
 import weakref
2
 from datetime import datetime, timedelta
5
 from datetime import datetime, timedelta
3
 from discord import Guild, Member
6
 from discord import Guild, Member
4
 from discord.ext import commands
7
 from discord.ext import commands
5
 
8
 
6
-from cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
7
 from config import CONFIG
9
 from config import CONFIG
8
-from rscollections import AgeBoundList
9
-from storage import Storage
10
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
11
+from rocketbot.collections import AgeBoundList
12
+from rocketbot.storage import Storage
10
 
13
 
11
 class JoinRaidContext:
14
 class JoinRaidContext:
12
 	"""
15
 	"""

cogs/patterncog.py → rocketbot/cogs/patterncog.py Переглянути файл

1
+"""
2
+Cog for matching messages against guild-configurable criteria and taking
3
+automated actions on them.
4
+"""
1
 import re
5
 import re
2
-from abc import ABC, abstractmethod
6
+from abc import ABCMeta, abstractmethod
3
 from discord import Guild, Member, Message
7
 from discord import Guild, Member, Message
4
 from discord.ext import commands
8
 from discord.ext import commands
5
 
9
 
6
-from cogs.basecog import BaseCog, BotMessage, BotMessageReaction
7
 from config import CONFIG
10
 from config import CONFIG
8
-from rbutils import parse_timedelta
9
-from storage import Storage
11
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction
12
+from rocketbot.storage import Storage
13
+from rocketbot.utils import parse_timedelta
10
 
14
 
11
 class PatternAction:
15
 class PatternAction:
12
 	"""
16
 	"""
20
 		arg_str = ', '.join(self.arguments)
24
 		arg_str = ', '.join(self.arguments)
21
 		return f'{self.action}({arg_str})'
25
 		return f'{self.action}({arg_str})'
22
 
26
 
23
-class PatternExpression(ABC):
27
+class PatternExpression(metaclass=ABCMeta):
24
 	"""
28
 	"""
25
 	Abstract message matching expression.
29
 	Abstract message matching expression.
26
 	"""
30
 	"""
165
 					try:
169
 					try:
166
 						ps = PatternCompiler.parse_statement(name, statement)
170
 						ps = PatternCompiler.parse_statement(name, statement)
167
 						patterns[name] = ps
171
 						patterns[name] = ps
168
-					except Exception as e:
169
-						self.log(guild, f'Error parsing saved statement "{name}". Skipping: {statement}. Error: {e}')
172
+					except PatternError as e:
173
+						self.log(guild, 'Error parsing saved statement ' + \
174
+							f'"{name}": "{e}" Statement: {statement}')
170
 			Storage.set_state_value(guild, 'PatternCog.patterns', patterns)
175
 			Storage.set_state_value(guild, 'PatternCog.patterns', patterns)
171
 		return patterns
176
 		return patterns
172
 
177
 
302
 			await context.message.reply(
307
 			await context.message.reply(
303
 				f'{CONFIG["success_emoji"]} Pattern `{name}` added.',
308
 				f'{CONFIG["success_emoji"]} Pattern `{name}` added.',
304
 				mention_author=False)
309
 				mention_author=False)
305
-		except Exception as e:
310
+		except PatternError as e:
306
 			await context.message.reply(
311
 			await context.message.reply(
307
 				f'{CONFIG["failure_emoji"]} Error parsing statement. {e}',
312
 				f'{CONFIG["failure_emoji"]} Error parsing statement. {e}',
308
 				mention_author=False)
313
 				mention_author=False)
339
 			msg += f'Pattern `{name}`:\n```\n{statement.original}\n```\n'
344
 			msg += f'Pattern `{name}`:\n```\n{statement.original}\n```\n'
340
 		await context.message.reply(msg, mention_author=False)
345
 		await context.message.reply(msg, mention_author=False)
341
 
346
 
347
+class PatternError(RuntimeError):
348
+	"""
349
+	Error thrown when parsing a pattern statement.
350
+	"""
351
+
342
 class PatternCompiler:
352
 class PatternCompiler:
343
 	"""
353
 	"""
344
 	Parses a user-provided message filter statement into a PatternStatement.
354
 	Parses a user-provided message filter statement into a PatternStatement.
457
 					in_quote = ch
467
 					in_quote = ch
458
 					current_token = ch
468
 					current_token = ch
459
 				elif ch == '\\':
469
 				elif ch == '\\':
460
-					raise RuntimeError("Unexpected \\")
470
+					raise PatternError("Unexpected \\ outside quoted string")
461
 				elif ch in cls.WHITESPACE_CHARS:
471
 				elif ch in cls.WHITESPACE_CHARS:
462
 					if len(current_token) > 0:
472
 					if len(current_token) > 0:
463
 						tokens.append(current_token)
473
 						tokens.append(current_token)
528
 				return (actions, token_index)
538
 				return (actions, token_index)
529
 			elif token == ',':
539
 			elif token == ',':
530
 				if len(current_action_tokens) < 1:
540
 				if len(current_action_tokens) < 1:
531
-					raise RuntimeError('Unexpected ,')
541
+					raise PatternError('Unexpected ,')
532
 				a = PatternAction(current_action_tokens[0], current_action_tokens[1:])
542
 				a = PatternAction(current_action_tokens[0], current_action_tokens[1:])
533
 				cls.__validate_action(a)
543
 				cls.__validate_action(a)
534
 				actions.append(a)
544
 				actions.append(a)
536
 			else:
546
 			else:
537
 				current_action_tokens.append(token)
547
 				current_action_tokens.append(token)
538
 			token_index += 1
548
 			token_index += 1
539
-		raise RuntimeError('Unexpected end of line')
549
+		raise PatternError('Unexpected end of line in action list')
540
 
550
 
541
 	@classmethod
551
 	@classmethod
542
 	def __validate_action(cls, action: PatternAction) -> None:
552
 	def __validate_action(cls, action: PatternAction) -> None:
543
 		args = cls.ACTION_TO_ARGS.get(action.action)
553
 		args = cls.ACTION_TO_ARGS.get(action.action)
544
 		if args is None:
554
 		if args is None:
545
-			raise RuntimeError(f'Unknown action "{action.action}"')
555
+			raise PatternError(f'Unknown action "{action.action}"')
546
 		if len(action.arguments) != len(args):
556
 		if len(action.arguments) != len(args):
547
 			if len(args) == 0:
557
 			if len(args) == 0:
548
-				raise RuntimeError(f'Action "{action.action}" expects no arguments, ' + \
558
+				raise PatternError(f'Action "{action.action}" expects no arguments, ' + \
549
 					f'got {len(action.arguments)}.')
559
 					f'got {len(action.arguments)}.')
550
 			else:
560
 			else:
551
-				raise RuntimeError(f'Action "{action.action}" expects {len(args)} ' + \
561
+				raise PatternError(f'Action "{action.action}" expects {len(args)} ' + \
552
 					f'arguments, got {len(action.arguments)}.')
562
 					f'arguments, got {len(action.arguments)}.')
553
 		for i, datatype in enumerate(args):
563
 		for i, datatype in enumerate(args):
554
 			action.arguments[i] = cls.parse_value(action.arguments[i], datatype)
564
 			action.arguments[i] = cls.parse_value(action.arguments[i], datatype)
573
 				if len(subexpressions) == 1:
583
 				if len(subexpressions) == 1:
574
 					return (subexpressions[0], token_index)
584
 					return (subexpressions[0], token_index)
575
 				if len(subexpressions) > 1:
585
 				if len(subexpressions) > 1:
576
-					raise RuntimeError('Too many subexpressions')
586
+					raise PatternError('Too many subexpressions')
577
 			compound_operator = None
587
 			compound_operator = None
578
 			if tokens[token_index] == ')':
588
 			if tokens[token_index] == ')':
579
 				if len(subexpressions) == 0:
589
 				if len(subexpressions) == 0:
580
-					raise RuntimeError('No subexpressions')
590
+					raise PatternError('No subexpressions')
581
 				if len(subexpressions) == 1:
591
 				if len(subexpressions) == 1:
582
 					return (subexpressions[0], token_index)
592
 					return (subexpressions[0], token_index)
583
 				return (PatternCompoundExpression(last_compound_operator, subexpressions), token_index)
593
 				return (PatternCompoundExpression(last_compound_operator, subexpressions), token_index)
597
 			elif tokens[token_index] == '(':
607
 			elif tokens[token_index] == '(':
598
 				(exp, next_index) = cls.read_expression(tokens, token_index + 1, depth + 1)
608
 				(exp, next_index) = cls.read_expression(tokens, token_index + 1, depth + 1)
599
 				if tokens[next_index] != ')':
609
 				if tokens[next_index] != ')':
600
-					raise RuntimeError('Expected )')
610
+					raise PatternError('Expected )')
601
 				subexpressions.append(exp)
611
 				subexpressions.append(exp)
602
 				token_index = next_index + 1
612
 				token_index = next_index + 1
603
 			else:
613
 			else:
605
 				subexpressions.append(simple)
615
 				subexpressions.append(simple)
606
 				token_index = next_index
616
 				token_index = next_index
607
 		if len(subexpressions) == 0:
617
 		if len(subexpressions) == 0:
608
-			raise RuntimeError('No subexpressions')
618
+			raise PatternError('No subexpressions')
609
 		elif len(subexpressions) == 1:
619
 		elif len(subexpressions) == 1:
610
 			return (subexpressions[0], token_index)
620
 			return (subexpressions[0], token_index)
611
 		else:
621
 		else:
619
 		the token index it left off at.
629
 		the token index it left off at.
620
 		"""
630
 		"""
621
 		if depth > 8:
631
 		if depth > 8:
622
-			raise RuntimeError('Expression nests too deeply')
632
+			raise PatternError('Expression nests too deeply')
623
 		if token_index >= len(tokens):
633
 		if token_index >= len(tokens):
624
-			raise RuntimeError('Expected field name, found EOL')
634
+			raise PatternError('Expected field name, found EOL')
625
 		field = tokens[token_index]
635
 		field = tokens[token_index]
626
 		token_index += 1
636
 		token_index += 1
627
 
637
 
628
 		datatype = cls.FIELD_TO_TYPE.get(field)
638
 		datatype = cls.FIELD_TO_TYPE.get(field)
629
 		if datatype is None:
639
 		if datatype is None:
630
-			raise RuntimeError(f'No such field "{field}"')
640
+			raise PatternError(f'No such field "{field}"')
631
 
641
 
632
 		if token_index >= len(tokens):
642
 		if token_index >= len(tokens):
633
-			raise RuntimeError('Expected operator, found EOL')
643
+			raise PatternError('Expected operator, found EOL')
634
 		op = tokens[token_index]
644
 		op = tokens[token_index]
635
 		token_index += 1
645
 		token_index += 1
636
 
646
 
637
 		if op == '!':
647
 		if op == '!':
638
 			if token_index >= len(tokens):
648
 			if token_index >= len(tokens):
639
-				raise RuntimeError('Expected operator, found EOL')
649
+				raise PatternError('Expected operator, found EOL')
640
 			op = '!' + tokens[token_index]
650
 			op = '!' + tokens[token_index]
641
 			token_index += 1
651
 			token_index += 1
642
 
652
 
643
 		allowed_ops = cls.TYPE_TO_OPERATORS[datatype]
653
 		allowed_ops = cls.TYPE_TO_OPERATORS[datatype]
644
 		if op not in allowed_ops:
654
 		if op not in allowed_ops:
645
 			if op in cls.OPERATORS_ALL:
655
 			if op in cls.OPERATORS_ALL:
646
-				raise RuntimeError(f'Operator {op} cannot be used with field "{field}"')
647
-			else:
648
-				raise RuntimeError(f'Unrecognized operator "{op}" - allowed: {list(allowed_ops)}')
656
+				raise PatternError(f'Operator {op} cannot be used with field "{field}"')
657
+			raise PatternError(f'Unrecognized operator "{op}" - allowed: {list(allowed_ops)}')
649
 
658
 
650
 		if token_index >= len(tokens):
659
 		if token_index >= len(tokens):
651
-			raise RuntimeError('Expected value, found EOL')
660
+			raise PatternError('Expected value, found EOL')
652
 		value = tokens[token_index]
661
 		value = tokens[token_index]
653
 
662
 
654
-		value = cls.parse_value(value, datatype)
663
+		try:
664
+			value = cls.parse_value(value, datatype)
665
+		except ValueError as cause:
666
+			raise PatternError(f'Bad value {value}') from cause
655
 
667
 
656
 		token_index += 1
668
 		token_index += 1
657
 		exp = PatternSimpleExpression(field, op, value)
669
 		exp = PatternSimpleExpression(field, op, value)
660
 	@classmethod
672
 	@classmethod
661
 	def parse_value(cls, value: str, datatype: str):
673
 	def parse_value(cls, value: str, datatype: str):
662
 		"""
674
 		"""
663
-		Converts a value token to its Python value.
675
+		Converts a value token to its Python value. Raises ValueError on failure.
664
 		"""
676
 		"""
665
 		if datatype == cls.TYPE_ID:
677
 		if datatype == cls.TYPE_ID:
666
 			p = re.compile('^[0-9]+$')
678
 			p = re.compile('^[0-9]+$')

cogs/urlspamcog.py → rocketbot/cogs/urlspamcog.py Переглянути файл

1
+"""
2
+Cog for detecting URLs posted by new users.
3
+"""
1
 import re
4
 import re
2
 from datetime import timedelta
5
 from datetime import timedelta
3
 from discord import Member, Message
6
 from discord import Member, Message
4
 from discord.ext import commands
7
 from discord.ext import commands
5
 
8
 
6
-from cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
7
 from config import CONFIG
9
 from config import CONFIG
8
-from rbutils import describe_timedelta
10
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
11
+from rocketbot.utils import describe_timedelta
9
 
12
 
10
 class URLSpamContext:
13
 class URLSpamContext:
11
 	"""
14
 	"""

rscollections.py → rocketbot/collections.py Переглянути файл


storage.py → rocketbot/storage.py Переглянути файл


rbutils.py → rocketbot/utils.py Переглянути файл

1
+"""
2
+General utility functions.
3
+"""
1
 import re
4
 import re
2
 from datetime import timedelta
5
 from datetime import timedelta
3
 
6
 

Завантаження…
Відмінити
Зберегти