-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodels.py
347 lines (258 loc) · 12.4 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
from datetime import datetime, timedelta
from typing import NamedTuple, Optional
import discord
from discord import Guild, Intents, Member, app_commands, Client
# from discord.ext.commands import Bot
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel import Field, Relationship, SQLModel, select, update, delete
from sqlmodel.ext.asyncio.session import AsyncSession
import aiofiles
import yaml
from taskmaster import suppress, amap
from sqlalchemy.orm import selectinload
global PRUNE_DATE, CREATION_DATE_LIMIT
# ONE Prune A day keeps the assholes away...
# Prune Date After Joining guild, you can edit that as required
PRUNE_DATE = timedelta(days=1)
# Creation date After Creating discord account (About 6 months)
CREATION_DATE_LIMIT = timedelta(weeks=26)
class IDModel(SQLModel):
"""Subclass for applying primary keys accross multiple tables"""
id: Optional[int] = Field(default=None, primary_key=True)
# === PRUNING ===
class GuildModel(IDModel, table=True):
guild_id: int = Field(unique=True)
prune_role_id: Optional[int] = None
"""Role ID for pruning users into a seperate private \
channel that only moderators and admins can see..."""
moderator_channel: Optional[int] = None
"""Where the bot needs to report pruned users to"""
pruned_members: list["PrunedMember"] = Relationship(back_populates="guild")
class PrunedMember(IDModel, table=True):
"""A Member that is scheduled for pruning"""
member_id: int
"""Discord Snowflake, the user could be apart of multiple \
guilds ready to be pruned hence not being a unqiue key"""
prune_date: datetime
"""The time to ban or remove a member from the server for\
likely being an alt account or spammer"""
reason: Optional[str] = None
guild_id: Optional[int] = Field(default=None, foreign_key="guildmodel.id")
"""The id for the guild in the SQL-Database"""
guild: Optional[GuildModel] = Relationship(back_populates="pruned_members")
"""The Guild assigned for the pruned member to get rid of\
on ban-wipe"""
# ======================== LOCKDOWNS ========================
# Inspired by EvilPauze (https://github.com/Alex1304/evilpauze) meant to lockdown and also
# fully recover factory settings of a channel when channel lockdown is considered nessesary
# This is essentially an attempt at upgrading these protocols...
# Since I am a guy who is no stranger to aggressive users I considered implementing EvilPauze
# into my bot to make it more aggressive.
# I guess you can call this EvilCalloc if you'd like...
class LockdownChannel(IDModel, table=True):
"""A Channel that is considered to be on-lockdown"""
channel_id:int
guild_id:int
"""Text id channel being locked down"""
date : datetime = datetime.now()
"""Timestamp of a lockdown good for record-keeping..."""
reason:Optional[str] = None
"""Reason for locking down the guild's text channel"""
roles: list["LockdownRole"] = Relationship(back_populates="channel")
def add_role(self, role:discord.Role):
self.roles.append(LockdownRole(role_id=role.id))
class LockdownRole(IDModel, table=True):
role_id:int
"""Discord Role ID being locked and unable to write text... (These can be filtered by role via command...)"""
channel_id : Optional[int] = Field(default=None, foreign_key="lockdownchannel.id")
channel:Optional[LockdownChannel] = Relationship(back_populates="roles")
class MissingPruneRole(Exception):
"""Owner/Admin didn't set Pruning Roles"""
pass
class MissingPruneRole(Exception):
"""Owner/Admin didn't set Pruning Roles"""
pass
class PrunedUser(NamedTuple):
member: Member
pruned_info: PrunedMember
class Defender(Client):
def __init__(
self, prefix = "?", intents=Intents.all(), dbname: str = "sqlite+aiosqlite:///defender.db"
) -> None:
super().__init__(intents=intents)
self.engine = create_async_engine(dbname)
self.session = async_sessionmaker(
self.engine, class_=AsyncSession, expire_on_commit=False
)
self.tree = app_commands.CommandTree(self)
self.command = self.tree.command
async def sync_guild(self, id:int):
"""Syncs a guild-id to the enabled servers the bot is allowed to be in"""
# You should configure these manually as a safety-mechanism...
guildObject = discord.Object(id=id)
self.tree.copy_global_to(guild=guildObject)
return await self.tree.sync(guild=guildObject)
async def setup_hook(self):
async with aiofiles.open("config.yaml", "r") as cfg:
data:dict[str] = yaml.safe_load(await cfg.read())
for i in data["discordServerIds"]:
await self.sync_guild(i)
# SETUP GLOBALS UNLESS DEFAULTS ARE GIVEN...
# TODO:
# if data.get("prune-time"):
# PRUNE_DATE = timedelta(**data["prune-time"])
# if data.get("creation-date-limit"):
# CREATION_DATE_LIMIT = timedelta(**data["creation-date-limit"])
async def init_db(self):
async with self.engine.begin() as e:
await e.run_sync(IDModel.metadata.create_all)
async def get_guild_model(self, guild_id: int) -> GuildModel:
async with self.session() as session:
scalar = await session.exec(
select(GuildModel).where(GuildModel.guild_id == guild_id)
)
guild = scalar.one_or_none()
if not guild:
guild = await session.merge(GuildModel(guild_id=guild_id))
await session.commit()
return guild
async def update_guild_prune_role(self, prune_role_id: int, guild_id: int):
"""Update current guild's prune role"""
async with self.session() as session:
await session.exec(
update(GuildModel)
.where(GuildModel.guild_id == guild_id)
.values(prune_role_id=prune_role_id)
)
await session.commit()
return await self.get_guild_model(guild_id)
async def update_guild_mod_channel(self, moderator_channel_id: int, guild_id: int):
"""Update current guild's prune role"""
async with self.session() as session:
await session.exec(
update(GuildModel)
.where(GuildModel.guild_id == guild_id)
.values(moderator_channel=moderator_channel_id)
)
await session.commit()
return await self.get_guild_model(guild_id)
async def get_pruned_member(self, snowflake: int, guild_id: int):
async with self.session() as session:
scalar = await session.exec(
select(PrunedMember)
.where(PrunedMember.member_id == snowflake)
.where(PrunedMember.guild_id == guild_id)
)
pinfo = scalar.one_or_none()
if not pinfo:
return
guild = self.get_guild(pinfo.guild_id)
member = guild.get_member(pinfo.member_id)
return PrunedUser(member, pinfo)
async def prune_member(self, member: Member, reason: str = "Suspicious account"):
"""Applies pruned role to an existing member and will be awaiting execution/ban"""
guildmodel = await self.get_guild_model(member.guild.id)
await member.add_roles(guildmodel.prune_role_id, reason=reason)
async with self.session() as s:
pm = await s.merge(
PrunedMember(
member_id=member.id,
prune_date=datetime.now() + PRUNE_DATE,
guild_id=member.guild.id,
reason=reason,
)
)
await s.commit()
return pm
async def ban_pruned_members(self, guild_id: int):
"""Bans all members in a guild when the given deadline is met"""
guild = self.get_guild(guild_id)
gm = await self.get_guild_model(guild_id)
channel = guild.get_channel(gm.moderator_channel)
assert channel, "This command requires A Moderation channel"
async with self.session() as s:
scalar = await s.exec(
select(PrunedMember)
.where(PrunedMember.guild_id == guild_id)
.where(PrunedMember.prune_date < datetime.now())
)
for user in scalar:
async with suppress(discord.NotFound, discord.Forbidden, discord.HTTPException):
# byebye asshole
await guild.ban(user.member_id, reason="Pruned For Suspicous Join/Behavior")
await s.delete(user)
await s.commit()
async def ban_all_pruned_members(self, guild_id:int):
"""Bans all members in a guild whithout the deadline"""
guild = self.get_guild(guild_id)
gm = await self.get_guild_model(guild_id)
channel = guild.get_channel(gm.moderator_channel)
assert channel, "This command requires A Moderation channel"
async with self.session() as s:
scalar = await s.exec(
select(PrunedMember)
.where(PrunedMember.guild_id == guild_id)
)
for user in scalar:
async with suppress(discord.NotFound, discord.Forbidden, discord.HTTPException):
if member := guild.get_member(user.member_id):
# Byebye asshole...
try:
await member.ban(reason="Pruned For Suspicous Join/Behavior")
except:
await s.delete(user)
await s.commit()
else:
await guild.ban(user.member_id, reason="Pruned For Suspicous Join/Behavior")
await s.delete(user)
await s.commit()
async def check_member(self, member:Member):
if member.created_at > (datetime.now() - CREATION_DATE_LIMIT):
await self.prune_member(member)
guild = self.get_guild(member.guild.id)
gm = await self.get_guild_model(guild)
await guild.get_channel(gm.moderator_channel).send(f"Pruned Member named:{member.name} DeveloperID: {member.id}")
async def remove_guild_model(self, guild:Guild):
"""Removes Guild and Pruned memebers scheduled for ban"""
async with self.session() as s:
await s.exec(delete(GuildModel).where(GuildModel.id == guild.id))
await s.commit()
async def create_guild_model(self, guild:Guild):
"""Creates a New guild Model"""
async with self.session() as s:
await s.merge(GuildModel(guild_id=guild.id))
await s.commit()
async def create_lockdown(self, guild:Guild, channel:discord.TextChannel):
"""Simillar to `EvilPauze` This will essentially create a lock for locking down and unlocking roles/settings from..."""
async with self.session() as s:
ldc = await s.merge(LockdownChannel(guild_id=guild.id, channel_id=channel.id))
await s.commit()
return ldc
async def get_lockdown(self, guild:Guild, channel:discord.TextChannel):
async with self.session() as s:
scalar = await s.exec(
select(LockdownChannel)
.where(LockdownChannel.guild_id == guild.id)
.where(LockdownChannel.channel_id == channel.id)
)
ld_channel = scalar.one_or_none()
return ld_channel
async def update_lockdown_role(self, ldc:LockdownChannel):
async with self.session() as s:
await s.merge(ldc)
await s.commit()
async def remove_lockdown(self, guild:Guild, channel:discord.TextChannel):
async with self.session() as s:
scalar = await s.exec(
select(LockdownChannel)
.where(LockdownChannel.guild_id == guild.id)
.where(LockdownChannel.channel_id == channel.id)
# selectinload is the important part otherwise no roles will be loaded and an error is thrown.
.options(selectinload(LockdownChannel.roles))
)
ldc = scalar.one_or_none()
for role in ldc.roles:
if dsc_role := guild.get_role(role.role_id):
await channel.set_permissions(dsc_role, send_messages=True)
await s.delete(ldc)
await s.commit()