Quellcode durchsuchen

Adding deletemessages command

tags/1.0.2
Rocketsoup vor 4 Jahren
Ursprung
Commit
a70f553157
4 geänderte Dateien mit 119 neuen und 21 gelöschten Zeilen
  1. 55
    0
      cogs/generalcog.py
  2. 4
    20
      cogs/patterncog.py
  3. 1
    1
      patterns.md
  4. 59
    0
      rbutils.py

+ 55
- 0
cogs/generalcog.py Datei anzeigen

@@ -1,5 +1,11 @@
1
+from datetime import datetime, timedelta
2
+from discord import Member, Message
1 3
 from discord.ext import commands
4
+import re
5
+
2 6
 from cogs.basecog import BaseCog, BotMessage
7
+from config import CONFIG
8
+from rbutils import parse_timedelta, describe_timedelta
3 9
 from storage import ConfigKey, Storage
4 10
 
5 11
 class GeneralCog(BaseCog):
@@ -50,3 +56,52 @@ class GeneralCog(BaseCog):
50 56
 	@commands.guild_only()
51 57
 	async def shutdown(self, context):
52 58
 		await self.bot.close()
59
+
60
+	@commands.command(
61
+		brief='Mass deletes messages',
62
+		description='Deletes recent messages by the given user. The age is ' +
63
+			'a duration, such as "30s", "5m", "1h30m". Messages far back in ' +
64
+			'the scrollback might not be deleted by this command.',
65
+		usage='<user> <age>'
66
+	)
67
+	@commands.has_permissions(manage_messages=True)
68
+	@commands.guild_only()
69
+	async def deletemessages(self, context, user: str, age: str) -> None:
70
+		member_id = self.__parse_member_id(user)
71
+		if member_id is None:
72
+			await context.message.reply(
73
+				f'{CONFIG["failure_emoji"]} user must be a mention or numeric user id',
74
+				mention_author=False)
75
+			return
76
+		try:
77
+			age_delta: timedelta = parse_timedelta(age)
78
+		except ValueError:
79
+			await context.message.reply(
80
+				f'{CONFIG["failure_emoji"]} age must be a timespan, like "30s", "10m", "1h30m"',
81
+				mention_author=False)
82
+			return
83
+		cutoff: datetime = datetime.utcnow() - age_delta
84
+		def predicate(message: Message) -> bool:
85
+			return str(message.author.id) == member_id and message.created_at >= cutoff
86
+		deleted_messages = []
87
+		for channel in context.guild.text_channels:
88
+			try:
89
+				deleted_messages += await channel.purge(limit=100, check=predicate)
90
+			except:
91
+				# XXX: Sloppily glossing over access errors instead of checking access
92
+				pass
93
+		await context.message.reply(
94
+			f'{CONFIG["success_emoji"]} Deleted {len(deleted_messages)} ' + \
95
+			f'messages by <@!{member_id}> from the past {describe_timedelta(age_delta)}.',
96
+			mention_author=False)
97
+
98
+	def __parse_member_id(self, arg: str) -> str:
99
+		p = re.compile('^<@!?([0-9]+)>$')
100
+		m = p.match(arg)
101
+		if m:
102
+			return m.group(1)
103
+		p = re.compile('^([0-9]+)$')
104
+		m = p.match(arg)
105
+		if m:
106
+			return m.group(1)
107
+		return None

+ 4
- 20
cogs/patterncog.py Datei anzeigen

@@ -6,6 +6,7 @@ import re
6 6
 
7 7
 from cogs.basecog import BaseCog, BotMessage, BotMessageReaction
8 8
 from config import CONFIG
9
+from rbutils import parse_timedelta
9 10
 from storage import Storage
10 11
 
11 12
 class PatternAction:
@@ -373,6 +374,8 @@ class PatternCompiler:
373 374
 		for cmd in command_chain:
374 375
 			if pattern_str.startswith(cmd):
375 376
 				pattern_str = pattern_str[len(cmd):].lstrip()
377
+			elif pattern_str.startswith(f'"{cmd}"'):
378
+				pattern_str = pattern_str[len(cmd) + 2:].lstrip()
376 379
 		return pattern_str
377 380
 
378 381
 	@classmethod
@@ -632,24 +635,5 @@ class PatternCompiler:
632 635
 		if type == cls.TYPE_FLOAT:
633 636
 			return float(value)
634 637
 		if type == cls.TYPE_TIMESPAN:
635
-			p = re.compile('^(?:[0-9]+[dhms])+$')
636
-			if p.match(value) is None:
637
-				raise RuntimeError("Illegal timespan value \"{value}\". Must be like \"100d\", \"5m30s\", etc.")
638
-			p = re.compile('([0-9]+)([dhms])')
639
-			days = 0
640
-			hours = 0
641
-			minutes = 0
642
-			seconds = 0
643
-			for m in p.finditer(value):
644
-				scalar = int(m.group(1))
645
-				unit = m.group(2)
646
-				if unit == 'd':
647
-					days = scalar
648
-				elif unit == 'h':
649
-					hours = scalar
650
-				elif unit == 'm':
651
-					minutes = scalar
652
-				elif unit == 's':
653
-					seconds = scalar
654
-			return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
638
+			return parse_timedelta(value)
655 639
 		raise ValueError(f'Unhandled datatype {datatype}')

+ 1
- 1
patterns.md Datei anzeigen

@@ -16,7 +16,7 @@ A statement consists of one or more actions to take on a message followed by an
16 16
 reply "You can't say that word!", delete if message contains "heck"
17 17
 ```
18 18
 
19
-This statement has two actions: replying, then deleting the original message. It will match any word that contains the word "heck".
19
+This statement has two actions: replying, then deleting the original message. It will match any message that contains the word "heck".
20 20
 
21 21
 The list of actions and the matching expression are separated by the word "if", which must be present in every statement.
22 22
 

+ 59
- 0
rbutils.py Datei anzeigen

@@ -0,0 +1,59 @@
1
+from datetime import timedelta
2
+import re
3
+
4
+def parse_timedelta(s: str) -> timedelta:
5
+	"""
6
+	Parses a timespan. Format examples:
7
+	"30m"
8
+	"10s"
9
+	"90d"
10
+	"1h30m"
11
+	"73d18h22m52s"
12
+	"""
13
+	p = re.compile('^(?:[0-9]+[dhms])+$')
14
+	if p.match(s) is None:
15
+		raise ValueError("Illegal timespan value '{s}'.")
16
+	p = re.compile('([0-9]+)([dhms])')
17
+	days = 0
18
+	hours = 0
19
+	minutes = 0
20
+	seconds = 0
21
+	for m in p.finditer(s):
22
+		scalar = int(m.group(1))
23
+		unit = m.group(2)
24
+		if unit == 'd':
25
+			days = scalar
26
+		elif unit == 'h':
27
+			hours = scalar
28
+		elif unit == 'm':
29
+			minutes = scalar
30
+		elif unit == 's':
31
+			seconds = scalar
32
+	return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
33
+
34
+def describe_timedelta(td: timedelta, max_components: int = 2) -> str:
35
+	values = [
36
+		td.days,
37
+		td.seconds // 3600,
38
+		(td.seconds // 60) % 60,
39
+		td.seconds % 60,
40
+	]
41
+	units = [
42
+		'day' if values[0] == 1 else 'days',
43
+		'hour' if values[1] == 1 else 'hours',
44
+		'minute' if values[2] == 1 else 'minutes',
45
+		'second' if values[3] == 1 else 'seconds',
46
+	]
47
+	while len(values) > 1 and values[0] == 0:
48
+		values.pop(0)
49
+		units.pop(0)
50
+	if len(values) > max_components:
51
+		values = values[0:max_components]
52
+		units = units[0:max_components]
53
+	while len(values) > 1 and values[-1] == 0:
54
+		values.pop(-1)
55
+		units.pop(-1)
56
+	tokens = []
57
+	for i in range(len(values)):
58
+		tokens.append(f'{values[i]} {units[i]}')
59
+	return ' '.join(tokens)

Laden…
Abbrechen
Speichern