/[sudobot]/branches/8.x/src/services/TriggerService.ts
ViewVC logotype

Contents of /branches/8.x/src/services/TriggerService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 9423 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 /**
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 string[]).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 string[]).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) as (
172 trigger: TriggerType,
173 context: TriggerHandlerContext<boolean>
174 ) => Promise<unknown>;
175
176 if (handlers[trigger.type].startsWith("triggerMessage")) {
177 if (!context.message) {
178 throw new Error(
179 "Attempting to call a message trigger without specifying a message object inside the context. This is an internal error."
180 );
181 }
182 }
183
184 if (handlers[trigger.type].startsWith("trigger")) {
185 await callback(trigger, context);
186 }
187 }
188
189 async triggerMemberStatusUpdate(
190 trigger: Extract<TriggerType, { type: "member_status_update" }>,
191 { newPresence, oldPresence }: TriggerHandlerContext<false>
192 ) {
193 if (!newPresence || !oldPresence || (!trigger.must_contain && !trigger.must_not_contain)) {
194 return;
195 }
196
197 const oldStatus = oldPresence?.activities.find(a => a.type === ActivityType.Custom)?.state ?? "";
198 const newStatus = newPresence?.activities.find(a => a.type === ActivityType.Custom)?.state ?? "";
199
200 if (newPresence.status === "offline" || newPresence.status === "invisible") {
201 return;
202 }
203
204 if (oldStatus === newStatus) {
205 return;
206 }
207
208 if (trigger.must_contain) {
209 for (const string of trigger.must_contain) {
210 if (!newStatus.includes(string)) {
211 return;
212 }
213 }
214 }
215
216 if (trigger.must_not_contain) {
217 for (const string of trigger.must_not_contain) {
218 if (newStatus.includes(string)) {
219 return;
220 }
221 }
222 }
223
224 try {
225 if (trigger.action === "assign_role") {
226 await newPresence.member?.roles.add(trigger.roles);
227 } else if (trigger.action === "take_away_role") {
228 await newPresence.member?.roles.remove(trigger.roles);
229 }
230 } catch (e) {
231 logError(e);
232 }
233 }
234
235 async triggerMessageSticky(
236 trigger: Extract<TriggerType, { type: "sticky_message" }>,
237 { message }: TriggerHandlerContext<true>
238 ) {
239 if (!this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`]) {
240 this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`] = true;
241
242 setTimeout(async () => {
243 const lastStickyMessage = this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`];
244
245 if (lastStickyMessage) {
246 try {
247 await lastStickyMessage.delete();
248 this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`] = undefined;
249 } catch (e) {
250 logError(e);
251 return;
252 }
253 }
254
255 try {
256 const sentMessage = await message.channel.send({
257 content: trigger.message,
258 components:
259 trigger.buttons.length === 0
260 ? undefined
261 : [
262 new ActionRowBuilder<ButtonBuilder>().addComponents(
263 ...trigger.buttons.map(({ label, url }) =>
264 new ButtonBuilder().setStyle(ButtonStyle.Link).setURL(url).setLabel(label)
265 )
266 )
267 ]
268 });
269
270 this.lastStickyMessages[`${message.guildId!}_${message.channelId!}`] = sentMessage;
271 this.lastStickyMessageQueues[`${message.guildId!}_${message.channelId!}`] = false;
272 } catch (e) {
273 logError(e);
274 }
275 }, 2000);
276 }
277 }
278 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26