1 |
rakinar2 |
577 |
/** |
2 |
|
|
* This file is part of SudoBot. |
3 |
|
|
* |
4 |
|
|
* Copyright (C) 2021-2023 OSN Developers. |
5 |
|
|
* |
6 |
|
|
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
|
|
* under the terms of the GNU Affero General Public License as published by |
8 |
|
|
* the Free Software Foundation, either version 3 of the License, or |
9 |
|
|
* (at your option) any later version. |
10 |
|
|
* |
11 |
|
|
* SudoBot is distributed in the hope that it will be useful, but |
12 |
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
|
|
* GNU Affero General Public License for more details. |
15 |
|
|
* |
16 |
|
|
* You should have received a copy of the GNU Affero General Public License |
17 |
|
|
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
|
|
*/ |
19 |
|
|
|
20 |
|
|
import { |
21 |
|
|
ActionRowBuilder, |
22 |
|
|
ActivityType, |
23 |
|
|
ButtonBuilder, |
24 |
|
|
ButtonStyle, |
25 |
|
|
ClientEvents, |
26 |
|
|
GuildMember, |
27 |
|
|
Message, |
28 |
|
|
Presence, |
29 |
|
|
Snowflake |
30 |
|
|
} from "discord.js"; |
31 |
|
|
import Service from "../core/Service"; |
32 |
|
|
import { GatewayEventListener } from "../decorators/GatewayEventListener"; |
33 |
|
|
import { HasEventListeners } from "../types/HasEventListeners"; |
34 |
|
|
import { TriggerType } from "../types/TriggerSchema"; |
35 |
|
|
import { logError } from "../utils/logger"; |
36 |
|
|
|
37 |
|
|
export const name = "triggerService"; |
38 |
|
|
|
39 |
|
|
const handlers = { |
40 |
|
|
sticky_message: "triggerMessageSticky", |
41 |
|
|
member_status_update: "triggerMemberStatusUpdate" |
42 |
|
|
} satisfies Record<TriggerType["type"], Extract<keyof TriggerService, `trigger${string}`>>; |
43 |
|
|
|
44 |
|
|
const events = { |
45 |
|
|
sticky_message: ["messageCreate"], |
46 |
|
|
member_status_update: ["presenceUpdate"] |
47 |
|
|
} satisfies Record<TriggerType["type"], (keyof ClientEvents)[]>; |
48 |
|
|
|
49 |
|
|
type TriggerHandlerContext<M extends boolean = false> = { |
50 |
|
|
message: M extends false ? Message | undefined : M extends true ? Message : never; |
51 |
|
|
member?: GuildMember; |
52 |
|
|
newPresence?: Presence; |
53 |
|
|
oldPresence?: Presence | null; |
54 |
|
|
}; |
55 |
|
|
|
56 |
|
|
export default class TriggerService extends Service implements HasEventListeners { |
57 |
|
|
private readonly lastStickyMessages: Record<`${Snowflake}_${Snowflake}`, Message | undefined> = {}; |
58 |
|
|
private readonly lastStickyMessageQueues: Record<`${Snowflake}_${Snowflake}`, boolean> = {}; |
59 |
|
|
|
60 |
|
|
private config(guildId: Snowflake) { |
61 |
|
|
return this.client.configManager.config[guildId]?.auto_triggers; |
62 |
|
|
} |
63 |
|
|
|
64 |
|
|
@GatewayEventListener("presenceUpdate") |
65 |
|
|
onPresenceUpdate(oldPresence: Presence | null, newPresence: Presence) { |
66 |
|
|
if (newPresence?.user?.bot) { |
67 |
|
|
return false; |
68 |
|
|
} |
69 |
|
|
|
70 |
|
|
const config = this.config(newPresence?.guild?.id ?? ""); |
71 |
|
|
|
72 |
|
|
if (!config?.enabled) { |
73 |
|
|
return false; |
74 |
|
|
} |
75 |
|
|
|
76 |
|
|
this.processTriggers( |
77 |
|
|
config.triggers, |
78 |
|
|
{ |
79 |
|
|
roles: [...(newPresence?.member?.roles.cache.keys() ?? [])], |
80 |
|
|
userId: newPresence.user?.id, |
81 |
|
|
context: { |
82 |
|
|
message: undefined, |
83 |
|
|
oldPresence, |
84 |
|
|
newPresence |
85 |
|
|
} |
86 |
|
|
}, |
87 |
|
|
["presenceUpdate"] |
88 |
|
|
); |
89 |
|
|
} |
90 |
|
|
|
91 |
|
|
onMessageCreate(message: Message<boolean>) { |
92 |
|
|
if (message.author.bot) { |
93 |
|
|
return false; |
94 |
|
|
} |
95 |
|
|
|
96 |
|
|
const config = this.config(message.guildId!); |
97 |
|
|
|
98 |
|
|
if (!config?.enabled || config?.global_disabled_channels?.includes(message.channelId!)) { |
99 |
|
|
return false; |
100 |
|
|
} |
101 |
|
|
|
102 |
|
|
this.processMessageTriggers(message, config.triggers); |
103 |
|
|
return true; |
104 |
|
|
} |
105 |
|
|
|
106 |
|
|
processTriggers( |
107 |
|
|
triggers: TriggerType[], |
108 |
|
|
data: Parameters<typeof this.processTrigger<false>>[1], |
109 |
|
|
triggerEvents: (keyof ClientEvents)[] | undefined = undefined |
110 |
|
|
) { |
111 |
|
|
loop: for (const trigger of triggers) { |
112 |
|
|
if (triggerEvents !== undefined) { |
113 |
|
|
for (const triggerEvent of triggerEvents) { |
114 |
|
|
if (!(events[trigger.type] as any).includes(triggerEvent)) { |
115 |
|
|
continue loop; |
116 |
|
|
} |
117 |
|
|
} |
118 |
|
|
} |
119 |
|
|
|
120 |
|
|
this.processTrigger<boolean>(trigger, data).catch(logError); |
121 |
|
|
} |
122 |
|
|
} |
123 |
|
|
|
124 |
|
|
processMessageTriggers(message: Message, triggers: TriggerType[]) { |
125 |
|
|
for (const trigger of triggers) { |
126 |
|
|
if (!(events[trigger.type] as any).includes("messageCreate")) { |
127 |
|
|
continue; |
128 |
|
|
} |
129 |
|
|
|
130 |
|
|
this.processTrigger(trigger, { |
131 |
|
|
channelId: message.channelId!, |
132 |
|
|
roles: message.member!.roles.cache.keys(), |
133 |
|
|
userId: message.author.id, |
134 |
|
|
context: { |
135 |
|
|
message |
136 |
|
|
} |
137 |
|
|
}).catch(logError); |
138 |
|
|
} |
139 |
|
|
} |
140 |
|
|
|
141 |
|
|
async processTrigger<B extends true | false>( |
142 |
|
|
trigger: TriggerType, |
143 |
|
|
{ |
144 |
|
|
channelId, |
145 |
|
|
roles, |
146 |
|
|
userId, |
147 |
|
|
context |
148 |
|
|
}: { |
149 |
|
|
channelId?: string; |
150 |
|
|
userId?: string; |
151 |
|
|
roles?: IterableIterator<Snowflake> | Snowflake[]; |
152 |
|
|
context: TriggerHandlerContext<B>; |
153 |
|
|
} |
154 |
|
|
) { |
155 |
|
|
if (channelId && !trigger.enabled_channels.includes(channelId)) { |
156 |
|
|
return; |
157 |
|
|
} |
158 |
|
|
|
159 |
|
|
if (userId && trigger.ignore_users.includes(userId)) { |
160 |
|
|
return; |
161 |
|
|
} |
162 |
|
|
|
163 |
|
|
if (roles) { |
164 |
|
|
for (const roleId of roles) { |
165 |
|
|
if (trigger.ignore_roles.includes(roleId)) { |
166 |
|
|
return; |
167 |
|
|
} |
168 |
|
|
} |
169 |
|
|
} |
170 |
|
|
|
171 |
|
|
const callback = this[handlers[trigger.type]].bind(this); |
172 |
|
|
|
173 |
|
|
if (handlers[trigger.type].startsWith("triggerMessage")) { |
174 |
|
|
if (!context.message) { |
175 |
|
|
throw new Error( |
176 |
|
|
"Attempting to call a message trigger without specifying a message object inside the context. This is an internal error." |
177 |
|
|
); |
178 |
|
|
} |
179 |
|
|
} |
180 |
|
|
|
181 |
|
|
if (handlers[trigger.type].startsWith("trigger")) { |
182 |
|
|
await callback(trigger as any, context as any); |
183 |
|
|
} |
184 |
|
|
} |
185 |
|
|
|
186 |
|
|
async triggerMemberStatusUpdate( |
187 |
|
|
trigger: Extract<TriggerType, { type: "member_status_update" }>, |
188 |
|
|
{ newPresence, oldPresence }: TriggerHandlerContext<false> |
189 |
|
|
) { |
190 |
|
|
if (!newPresence || !oldPresence || (!trigger.must_contain && !trigger.must_not_contain)) { |
191 |
|
|
return; |
192 |
|
|
} |
193 |
|
|
|
194 |
|
|
const oldStatus = oldPresence?.activities.find(a => a.type === ActivityType.Custom)?.state ?? ""; |
195 |
|
|
const newStatus = newPresence?.activities.find(a => a.type === ActivityType.Custom)?.state ?? ""; |
196 |
|
|
|
197 |
|
|
if (newPresence.status === "offline" || newPresence.status === "invisible") { |
198 |
|
|
return; |
199 |
|
|
} |
200 |
|
|
|
201 |
|
|
if (oldStatus === newStatus) { |
202 |
|
|
return; |
203 |
|
|
} |
204 |
|
|
|
205 |
|
|
if (trigger.must_contain) { |
206 |
|
|
for (const string of trigger.must_contain) { |
207 |
|
|
if (!newStatus.includes(string)) { |
208 |
|
|
return; |
209 |
|
|
} |
210 |
|
|
} |
211 |
|
|
} |
212 |
|
|
|
213 |
|
|
if (trigger.must_not_contain) { |
214 |
|
|
for (const string of trigger.must_not_contain) { |
215 |
|
|
if (newStatus.includes(string)) { |
216 |
|
|
return; |
217 |
|
|
} |
218 |
|
|
} |
219 |
|
|
} |
220 |
|
|
|
221 |
|
|
try { |
222 |
|
|
if (trigger.action === "assign_role") { |
223 |
|
|
await newPresence.member?.roles.add(trigger.roles); |
224 |
|
|
} else if (trigger.action === "take_away_role") { |
225 |
|
|
await newPresence.member?.roles.remove(trigger.roles); |
226 |
|
|
} |
227 |
|
|
} catch (e) { |
228 |
|
|
logError(e); |
229 |
|
|
} |
230 |
|
|
} |
231 |
|
|
|
232 |
|
|
async triggerMessageSticky( |
233 |
|
|
trigger: Extract<TriggerType, { type: "sticky_message" }>, |
234 |
|
|
{ message }: TriggerHandlerContext<true> |
235 |
|
|
) { |
236 |
|
|
if (!this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`]) { |
237 |
|
|
this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`] = true; |
238 |
|
|
|
239 |
|
|
setTimeout(async () => { |
240 |
|
|
const lastStickyMessage = this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`]; |
241 |
|
|
|
242 |
|
|
if (lastStickyMessage) { |
243 |
|
|
try { |
244 |
|
|
await lastStickyMessage.delete(); |
245 |
|
|
this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`] = undefined; |
246 |
|
|
} catch (e) { |
247 |
|
|
logError(e); |
248 |
|
|
return; |
249 |
|
|
} |
250 |
|
|
} |
251 |
|
|
|
252 |
|
|
try { |
253 |
|
|
const sentMessage = await message.channel.send({ |
254 |
|
|
content: trigger.message, |
255 |
|
|
components: |
256 |
|
|
trigger.buttons.length === 0 |
257 |
|
|
? undefined |
258 |
|
|
: [ |
259 |
|
|
new ActionRowBuilder<ButtonBuilder>().addComponents( |
260 |
|
|
...trigger.buttons.map(({ label, url }) => |
261 |
|
|
new ButtonBuilder().setStyle(ButtonStyle.Link).setURL(url).setLabel(label) |
262 |
|
|
) |
263 |
|
|
) |
264 |
|
|
] |
265 |
|
|
}); |
266 |
|
|
|
267 |
|
|
this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`] = sentMessage; |
268 |
|
|
this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`] = false; |
269 |
|
|
} catch (e) { |
270 |
|
|
logError(e); |
271 |
|
|
} |
272 |
|
|
}, 2000); |
273 |
|
|
} |
274 |
|
|
} |
275 |
|
|
} |