|
|
@@ -1,165 +0,0 @@
|
|
1
|
|
-import weakref
|
|
2
|
|
-
|
|
3
|
|
-from datetime import datetime, timedelta, timezone
|
|
4
|
|
-from typing import Optional
|
|
5
|
|
-
|
|
6
|
|
-from discord import Guild, Member, Interaction
|
|
7
|
|
-from discord.app_commands import Group, guild_only, default_permissions, Transform
|
|
8
|
|
-from discord.ext.commands import Cog
|
|
9
|
|
-
|
|
10
|
|
-from config import CONFIG
|
|
11
|
|
-from rocketbot.bot import Rocketbot
|
|
12
|
|
-from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
|
|
13
|
|
-from rocketbot.collections import AgeBoundList
|
|
14
|
|
-from rocketbot.storage import Storage
|
|
15
|
|
-from rocketbot.utils import TimeDeltaTransformer, MOD_PERMISSIONS
|
|
16
|
|
-
|
|
17
|
|
-
|
|
18
|
|
-class JoinAgeQueryContext:
|
|
19
|
|
- """
|
|
20
|
|
- Data about a join age query
|
|
21
|
|
- """
|
|
22
|
|
- def __init__(self, join_members: list[Member], timespan: timedelta):
|
|
23
|
|
- self.join_members = list(join_members)
|
|
24
|
|
- self.timespan: timedelta = timespan
|
|
25
|
|
- self.kicked_members: set[Member] = set()
|
|
26
|
|
- self.banned_members: set[Member] = set()
|
|
27
|
|
- self.results_message_ref: Optional[weakref.ReferenceType[BotMessage]] = None
|
|
28
|
|
-
|
|
29
|
|
-class JoinAgeCog(BaseCog, name='Join Age'):
|
|
30
|
|
- """
|
|
31
|
|
- Cog for finding users by when they joined.
|
|
32
|
|
- """
|
|
33
|
|
- SETTING_ENABLED = CogSetting('enabled', bool,
|
|
34
|
|
- brief='join age',
|
|
35
|
|
- description='Whether this cog is enabled for a guild.')
|
|
36
|
|
- SETTING_JOIN_TIME = CogSetting('jointime', timedelta,
|
|
37
|
|
- brief='maximum length of time to track new joins',
|
|
38
|
|
- description='The number of seconds of join history to maintain.',
|
|
39
|
|
- usage='<seconds:float>',
|
|
40
|
|
- min_value=1.0)
|
|
41
|
|
-
|
|
42
|
|
- STATE_KEY_RECENT_JOINS = "JoinAgeCog.recent_joins"
|
|
43
|
|
-
|
|
44
|
|
- def __init__(self, bot: Rocketbot):
|
|
45
|
|
- super().__init__(
|
|
46
|
|
- bot,
|
|
47
|
|
- config_prefix='joinage',
|
|
48
|
|
- name='join age',
|
|
49
|
|
- short_description='Tracks recently joined users with options to mass kick or ban.',
|
|
50
|
|
- )
|
|
51
|
|
- self.add_setting(JoinAgeCog.SETTING_ENABLED)
|
|
52
|
|
- self.add_setting(JoinAgeCog.SETTING_JOIN_TIME)
|
|
53
|
|
-
|
|
54
|
|
- joinage = Group(
|
|
55
|
|
- name='joinage',
|
|
56
|
|
- description='Queries for users who joined in the past span of time',
|
|
57
|
|
- guild_only=True,
|
|
58
|
|
- default_permissions=MOD_PERMISSIONS,
|
|
59
|
|
- extras={
|
|
60
|
|
- 'long_description': 'Searches for users who joined the server recently. ' + \
|
|
61
|
|
- 'Can use time spans like 30s, 5m, 1h, 7d, etc.',
|
|
62
|
|
- 'usage': '<time_period>',
|
|
63
|
|
- }
|
|
64
|
|
- )
|
|
65
|
|
-
|
|
66
|
|
- @joinage.command(
|
|
67
|
|
- name='search',
|
|
68
|
|
- description='Searches for users who joined recently',
|
|
69
|
|
- )
|
|
70
|
|
- @guild_only()
|
|
71
|
|
- @default_permissions(ban_members=True)
|
|
72
|
|
- async def search(self, interaction: Interaction, timespan: Transform[timedelta, TimeDeltaTransformer]) -> None:
|
|
73
|
|
- """Command handler"""
|
|
74
|
|
- guild: Guild = interaction.guild
|
|
75
|
|
- recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
|
|
76
|
|
- if recent_joins is None:
|
|
77
|
|
- max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
|
|
78
|
|
- recent_joins = AgeBoundList(max_age, lambda i, member0 : member0.joined_at)
|
|
79
|
|
- Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
|
|
80
|
|
- results: list = []
|
|
81
|
|
- cutoff: datetime = datetime.now(timezone.utc) - timespan
|
|
82
|
|
- for member in recent_joins:
|
|
83
|
|
- if member.joined_at > cutoff:
|
|
84
|
|
- results.append(member)
|
|
85
|
|
- ctx = JoinAgeQueryContext(results, timespan)
|
|
86
|
|
- msg = BotMessage(guild,
|
|
87
|
|
- text='',
|
|
88
|
|
- type=BotMessage.TYPE_INFO,
|
|
89
|
|
- context=ctx)
|
|
90
|
|
- ctx.results_message_ref = weakref.ref(msg)
|
|
91
|
|
- await self.__update_results_message(ctx)
|
|
92
|
|
- await self.post_message(msg)
|
|
93
|
|
- await interaction.response.send_message(
|
|
94
|
|
- "Search started",
|
|
95
|
|
- ephemeral=True,
|
|
96
|
|
- )
|
|
97
|
|
-
|
|
98
|
|
- async def on_mod_react(self,
|
|
99
|
|
- bot_message: BotMessage,
|
|
100
|
|
- reaction: BotMessageReaction,
|
|
101
|
|
- reacted_by: Member) -> None:
|
|
102
|
|
- guild: Guild = bot_message.guild
|
|
103
|
|
- ctx: JoinAgeQueryContext = bot_message.context
|
|
104
|
|
- if reaction.emoji == CONFIG['kick_emoji']:
|
|
105
|
|
- to_kick = set(ctx.join_members) - ctx.kicked_members
|
|
106
|
|
- for member in to_kick:
|
|
107
|
|
- await member.kick(
|
|
108
|
|
- reason=f'Rocketbot: Mass kick based on join age, by {reacted_by.name}.')
|
|
109
|
|
- ctx.kicked_members |= to_kick
|
|
110
|
|
- await self.__update_results_message(ctx)
|
|
111
|
|
- self.log(guild, f'Users kicked by {reacted_by.name}.')
|
|
112
|
|
- elif reaction.emoji == CONFIG['ban_emoji']:
|
|
113
|
|
- to_ban = set(ctx.join_members) - ctx.banned_members
|
|
114
|
|
- for member in to_ban:
|
|
115
|
|
- await member.ban(
|
|
116
|
|
- reason=f'Rocketbot: Mass ban based on join age, by {reacted_by.name}.',
|
|
117
|
|
- delete_message_days=0)
|
|
118
|
|
- ctx.banned_members |= to_ban
|
|
119
|
|
- await self.__update_results_message(ctx)
|
|
120
|
|
- self.log(guild, f'Users banned by {reacted_by.name}')
|
|
121
|
|
-
|
|
122
|
|
- @Cog.listener()
|
|
123
|
|
- async def on_member_join(self, member: Member) -> None:
|
|
124
|
|
- """Event handler"""
|
|
125
|
|
- guild: Guild = member.guild
|
|
126
|
|
- if not self.get_guild_setting(guild, self.SETTING_ENABLED):
|
|
127
|
|
- return
|
|
128
|
|
- recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
|
|
129
|
|
- if recent_joins is None:
|
|
130
|
|
- max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
|
|
131
|
|
- recent_joins = AgeBoundList(max_age, lambda i, member : member.joined_at)
|
|
132
|
|
- Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
|
|
133
|
|
- recent_joins.append(member)
|
|
134
|
|
-
|
|
135
|
|
- async def __update_results_message(self, context: JoinAgeQueryContext) -> None:
|
|
136
|
|
- if context.results_message_ref is None:
|
|
137
|
|
- return
|
|
138
|
|
- bot_message = context.results_message_ref()
|
|
139
|
|
- if bot_message is None:
|
|
140
|
|
- return
|
|
141
|
|
- text = f'The following members joined in the last {context.timespan}\n\n'
|
|
142
|
|
- if len(context.join_members) > 0:
|
|
143
|
|
- max_members = CONFIG['max_members_per_message']
|
|
144
|
|
- for member in context.join_members[:max_members]:
|
|
145
|
|
- text += '\n• '
|
|
146
|
|
- if member in context.banned_members:
|
|
147
|
|
- text += f'~~{member.mention} ({member.id})~~ - banned'
|
|
148
|
|
- elif member in context.kicked_members:
|
|
149
|
|
- text += f'~~{member.mention} ({member.id})~~ - kicked'
|
|
150
|
|
- else:
|
|
151
|
|
- text += f'{member.mention} ({member.id})'
|
|
152
|
|
- if len(context.join_members) > max_members:
|
|
153
|
|
- text += f'\n• {len(context.join_members) - max_members} more'
|
|
154
|
|
- else:
|
|
155
|
|
- text += 'No members found. If the bot was recently restarted ' + \
|
|
156
|
|
- 'or JoinAgeCog just enabled, only new joins will be tracked.'
|
|
157
|
|
- await bot_message.set_text(text)
|
|
158
|
|
- if len(context.join_members) > 0:
|
|
159
|
|
- member_count = len(context.join_members)
|
|
160
|
|
- kick_count = len(context.kicked_members)
|
|
161
|
|
- ban_count = len(context.banned_members)
|
|
162
|
|
- await bot_message.set_reactions(BotMessageReaction.standard_set(
|
|
163
|
|
- did_kick=kick_count >= member_count,
|
|
164
|
|
- did_ban=ban_count >= member_count,
|
|
165
|
|
- user_count=member_count))
|