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

Contents of /branches/7.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: 9306 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 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 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26