|
|
@@ -5,7 +5,7 @@ import re
|
|
5
|
5
|
from datetime import timedelta
|
|
6
|
6
|
from typing import Literal
|
|
7
|
7
|
|
|
8
|
|
-from discord import Member, Message
|
|
|
8
|
+from discord import Guild, Member, Message
|
|
9
|
9
|
from discord import utils as discordutils
|
|
10
|
10
|
from discord.ext.commands import Cog
|
|
11
|
11
|
from discord.utils import escape_markdown
|
|
|
@@ -69,6 +69,14 @@ class URLSpamCog(BaseCog, name='URL Spam'):
|
|
69
|
69
|
enum_values={'nothing', 'modwarn', 'modwarndelete',
|
|
70
|
70
|
'chatwarn', 'chatwarndelete', 'delete', 'kick', 'ban'},
|
|
71
|
71
|
)
|
|
|
72
|
+ SETTING_IGNORED_DOMAINS = CogSetting(
|
|
|
73
|
+ 'ignoreddomains',
|
|
|
74
|
+ str,
|
|
|
75
|
+ default_value='tenor.com;giphy.com;imgur.com',
|
|
|
76
|
+ brief='List of allowed domains to exclude from detecting',
|
|
|
77
|
+ description='A semicolon-delimited list of domains that can be posted '
|
|
|
78
|
+ 'without triggering a warning. E.g. GIF hosting sites.'
|
|
|
79
|
+ )
|
|
72
|
80
|
|
|
73
|
81
|
def __init__(self, bot):
|
|
74
|
82
|
super().__init__(
|
|
|
@@ -80,79 +88,115 @@ class URLSpamCog(BaseCog, name='URL Spam'):
|
|
80
|
88
|
self.add_setting(URLSpamCog.SETTING_ACTION)
|
|
81
|
89
|
self.add_setting(URLSpamCog.SETTING_JOIN_AGE)
|
|
82
|
90
|
self.add_setting(URLSpamCog.SETTING_DECEPTIVE_ACTION)
|
|
|
91
|
+ self.add_setting(URLSpamCog.SETTING_IGNORED_DOMAINS)
|
|
83
|
92
|
|
|
84
|
93
|
@Cog.listener()
|
|
85
|
94
|
async def on_message(self, message: Message):
|
|
86
|
95
|
"""Event listener"""
|
|
87
|
96
|
if message.author is None or \
|
|
88
|
|
- message.author.bot or \
|
|
89
|
97
|
message.guild is None or \
|
|
90
|
98
|
message.channel is None or \
|
|
91
|
99
|
message.content is None:
|
|
|
100
|
+ self.__trace("Missing message data")
|
|
|
101
|
+ return
|
|
|
102
|
+ if message.author.bot:
|
|
|
103
|
+ self.__trace("Message from bot")
|
|
|
104
|
+ return
|
|
|
105
|
+ if message.channel.permissions_for(message.author).ban_members:
|
|
|
106
|
+ self.__trace("User exempt")
|
|
92
|
107
|
return
|
|
93
|
108
|
if not self.get_guild_setting(message.guild, self.SETTING_ENABLED):
|
|
|
109
|
+ self.__trace("Cog disabled")
|
|
|
110
|
+ return
|
|
|
111
|
+ urls = self.__find_urls(message.content)
|
|
|
112
|
+ if len(urls) == 0:
|
|
|
113
|
+ self.__trace("Contains no URLs")
|
|
|
114
|
+ return
|
|
|
115
|
+ if self.__all_ignored_domains(message.guild, urls):
|
|
|
116
|
+ self.__trace("All URLs are ignored domains")
|
|
94
|
117
|
return
|
|
95
|
118
|
await self.check_message_recency(message)
|
|
96
|
119
|
await self.check_deceptive_links(message)
|
|
97
|
120
|
|
|
|
121
|
+ def __all_ignored_domains(self, guild: Guild, urls: list[str]) -> bool:
|
|
|
122
|
+ domain_list: str = self.get_guild_setting(guild, self.SETTING_IGNORED_DOMAINS)
|
|
|
123
|
+ self.__trace(f"Domain list is {domain_list}")
|
|
|
124
|
+ domains = domain_list.split(';')
|
|
|
125
|
+ self.__trace(f"Split into {len(domains)} domains")
|
|
|
126
|
+ for url in urls:
|
|
|
127
|
+ if not self.__is_ignored_url(url, domains):
|
|
|
128
|
+ return False
|
|
|
129
|
+ return True
|
|
|
130
|
+
|
|
|
131
|
+ def __is_ignored_url(self, url: str, ignored_domains: list[str]) -> bool:
|
|
|
132
|
+ p = re.compile(r'https?://([\w\.]+)')
|
|
|
133
|
+ domain = re.search(p, url).group(1)
|
|
|
134
|
+ for ignored_domain in ignored_domains:
|
|
|
135
|
+ if domain.endswith(ignored_domain.strip()):
|
|
|
136
|
+ self.__trace(f"URL {url} matches ignored domain {ignored_domain}")
|
|
|
137
|
+ return True
|
|
|
138
|
+ self.__trace("URL {url} not ignored")
|
|
|
139
|
+ return False
|
|
|
140
|
+
|
|
98
|
141
|
async def check_message_recency(self, message: Message):
|
|
99
|
142
|
"""Checks if the message was sent too recently by a new user"""
|
|
100
|
143
|
action = self.get_guild_setting(message.guild, self.SETTING_ACTION)
|
|
101
|
144
|
join_seconds = self.get_guild_setting(message.guild, self.SETTING_JOIN_AGE)
|
|
102
|
145
|
min_join_age = timedelta(seconds=join_seconds)
|
|
103
|
146
|
if action == 'nothing':
|
|
104
|
|
- return
|
|
105
|
|
- if not self.__contains_url(message.content):
|
|
|
147
|
+ self.__trace("Configured action is nothing")
|
|
106
|
148
|
return
|
|
107
|
149
|
join_age = message.created_at - message.author.joined_at
|
|
108
|
150
|
join_age_str = describe_timedelta(join_age)
|
|
109
|
|
- if join_age < min_join_age:
|
|
110
|
|
- context = URLSpamContext(message)
|
|
111
|
|
- needs_attention = False
|
|
112
|
|
- if action == 'modwarn':
|
|
113
|
|
- needs_attention = not self.was_warned_recently(message.author)
|
|
114
|
|
- self.log(message.guild, f'New user {message.author.name} ' + \
|
|
115
|
|
- f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
116
|
|
- 'joining.' + (' Mods alerted.' if needs_attention else ''))
|
|
117
|
|
- elif action == 'delete':
|
|
118
|
|
- await message.delete()
|
|
119
|
|
- context.is_deleted = True
|
|
120
|
|
- self.log(message.guild, f'New user {message.author.name} ' + \
|
|
121
|
|
- f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
122
|
|
- 'joining. Message deleted.')
|
|
123
|
|
- elif action == 'kick':
|
|
124
|
|
- await message.delete()
|
|
125
|
|
- context.is_deleted = True
|
|
126
|
|
- await message.author.kick(
|
|
127
|
|
- reason=f'Rocketbot: Posted a link {join_age_str} after joining')
|
|
128
|
|
- context.is_kicked = True
|
|
129
|
|
- self.log(message.guild, f'New user {message.author.name} ' + \
|
|
130
|
|
- f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
131
|
|
- 'joining. User kicked.')
|
|
132
|
|
- elif action == 'ban':
|
|
133
|
|
- await message.author.ban(
|
|
134
|
|
- reason=f'Rocketbot: User posted a link {join_age_str} after joining',
|
|
135
|
|
- delete_message_days=1)
|
|
136
|
|
- context.is_deleted = True
|
|
137
|
|
- context.is_kicked = True
|
|
138
|
|
- context.is_banned = True
|
|
139
|
|
- self.log(message.guild, f'New user {message.author.name} ' + \
|
|
140
|
|
- f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
141
|
|
- 'joining. User banned.')
|
|
142
|
|
- bm = BotMessage(
|
|
143
|
|
- message.guild,
|
|
144
|
|
- f'User {message.author.mention} posted a URL ' + \
|
|
145
|
|
- f'{join_age_str} after joining: {message.jump_url}',
|
|
146
|
|
- type = BotMessage.TYPE_MOD_WARNING if needs_attention else BotMessage.TYPE_INFO,
|
|
147
|
|
- context = context)
|
|
148
|
|
- bm.quote = discordutils.remove_markdown(message.clean_content)
|
|
149
|
|
- await bm.set_reactions(BotMessageReaction.standard_set(
|
|
150
|
|
- did_delete=context.is_deleted,
|
|
151
|
|
- did_kick=context.is_kicked,
|
|
152
|
|
- did_ban=context.is_banned))
|
|
153
|
|
- await self.post_message(bm)
|
|
154
|
|
- if needs_attention:
|
|
155
|
|
- self.record_warning(message.author)
|
|
|
151
|
+ if join_age > min_join_age:
|
|
|
152
|
+ self.__trace("User has been member long enough")
|
|
|
153
|
+ return
|
|
|
154
|
+ context = URLSpamContext(message)
|
|
|
155
|
+ needs_attention = False
|
|
|
156
|
+ if action == 'modwarn':
|
|
|
157
|
+ needs_attention = not self.was_warned_recently(message.author)
|
|
|
158
|
+ self.log(message.guild, f'New user {message.author.name} ' + \
|
|
|
159
|
+ f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
|
160
|
+ 'joining.' + (' Mods alerted.' if needs_attention else ''))
|
|
|
161
|
+ elif action == 'delete':
|
|
|
162
|
+ await message.delete()
|
|
|
163
|
+ context.is_deleted = True
|
|
|
164
|
+ self.log(message.guild, f'New user {message.author.name} ' + \
|
|
|
165
|
+ f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
|
166
|
+ 'joining. Message deleted.')
|
|
|
167
|
+ elif action == 'kick':
|
|
|
168
|
+ await message.delete()
|
|
|
169
|
+ context.is_deleted = True
|
|
|
170
|
+ await message.author.kick(
|
|
|
171
|
+ reason=f'Rocketbot: Posted a link {join_age_str} after joining')
|
|
|
172
|
+ context.is_kicked = True
|
|
|
173
|
+ self.log(message.guild, f'New user {message.author.name} ' + \
|
|
|
174
|
+ f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
|
175
|
+ 'joining. User kicked.')
|
|
|
176
|
+ elif action == 'ban':
|
|
|
177
|
+ await message.author.ban(
|
|
|
178
|
+ reason=f'Rocketbot: User posted a link {join_age_str} after joining',
|
|
|
179
|
+ delete_message_days=1)
|
|
|
180
|
+ context.is_deleted = True
|
|
|
181
|
+ context.is_kicked = True
|
|
|
182
|
+ context.is_banned = True
|
|
|
183
|
+ self.log(message.guild, f'New user {message.author.name} ' + \
|
|
|
184
|
+ f'({message.author.id}) posted URL {join_age_str} after ' + \
|
|
|
185
|
+ 'joining. User banned.')
|
|
|
186
|
+ bm = BotMessage(
|
|
|
187
|
+ message.guild,
|
|
|
188
|
+ f'User {message.author.mention} posted a URL ' + \
|
|
|
189
|
+ f'{join_age_str} after joining: {message.jump_url}',
|
|
|
190
|
+ type = BotMessage.TYPE_MOD_WARNING if needs_attention else BotMessage.TYPE_INFO,
|
|
|
191
|
+ context = context)
|
|
|
192
|
+ bm.quote = discordutils.remove_markdown(message.clean_content)
|
|
|
193
|
+ await bm.set_reactions(BotMessageReaction.standard_set(
|
|
|
194
|
+ did_delete=context.is_deleted,
|
|
|
195
|
+ did_kick=context.is_kicked,
|
|
|
196
|
+ did_ban=context.is_banned))
|
|
|
197
|
+ await self.post_message(bm)
|
|
|
198
|
+ if needs_attention:
|
|
|
199
|
+ self.record_warning(message.author)
|
|
156
|
200
|
|
|
157
|
201
|
async def check_deceptive_links(self, message: Message):
|
|
158
|
202
|
"""
|
|
|
@@ -302,7 +346,16 @@ class URLSpamCog(BaseCog, name='URL Spam'):
|
|
302
|
346
|
did_kick=context.is_kicked,
|
|
303
|
347
|
did_ban=context.is_banned))
|
|
304
|
348
|
|
|
|
349
|
+ def __trace(self, message: str):
|
|
|
350
|
+ # print(f'URLSpamCog: {message}')
|
|
|
351
|
+ pass
|
|
|
352
|
+
|
|
305
|
353
|
@classmethod
|
|
306
|
354
|
def __contains_url(cls, text: str) -> bool:
|
|
307
|
355
|
p = re.compile(r'https?://\S+')
|
|
308
|
356
|
return p.search(text) is not None
|
|
|
357
|
+
|
|
|
358
|
+ @classmethod
|
|
|
359
|
+ def __find_urls(cls, text: str) -> list[str]:
|
|
|
360
|
+ p = re.compile(r'https?://\S+')
|
|
|
361
|
+ return re.findall(p, text)
|