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

Contents of /branches/8.x/src/services/AFKService.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: 9439 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 { AfkEntry } from "@prisma/client";
21 import { formatDistanceToNowStrict } from "date-fns";
22 import {
23 ChannelType,
24 Collection,
25 GuildMember,
26 Message,
27 Snowflake,
28 escapeMarkdown,
29 time
30 } from "discord.js";
31 import Service from "../core/Service";
32 import { GatewayEventListener } from "../decorators/GatewayEventListener";
33 import { HasEventListeners } from "../types/HasEventListeners";
34 import { log, logError } from "../utils/Logger";
35
36 export const name = "afkService";
37
38 export default class AFKService extends Service implements HasEventListeners {
39 protected readonly entries = new Collection<`${Snowflake | "global"}_${Snowflake}`, AfkEntry>();
40 protected readonly syncTimeoutDelay = 15_000;
41 protected syncTimeout: Timer | null = null;
42 protected readonly modifiedIds = new Set<`${Snowflake | "global"}_${Snowflake}`>();
43
44 @GatewayEventListener("ready")
45 async onReady() {
46 const entries = await this.client.prisma.afkEntry.findMany();
47
48 for (const entry of entries) {
49 this.entries.set(`${entry.global ? "global" : entry.guildId}_${entry.userId}`, entry);
50 }
51 }
52
53 isAFK(guildId: string, userId: string) {
54 return this.entries.has(`${guildId}_${userId}`) || this.entries.has(`global_${userId}`);
55 }
56
57 toggle(guildId: string, userId: string, reason?: string) {
58 if (this.isAFK(guildId, userId)) {
59 return this.removeAFK(guildId, userId);
60 }
61
62 return this.startAFK(guildId, userId, reason);
63 }
64
65 getGuildAFKs(guildId: Snowflake) {
66 return this.entries.filter(entry => entry.guildId === guildId && !entry.global);
67 }
68
69 async removeGuildAFKs(guildId: Snowflake) {
70 const entries = this.getGuildAFKs(guildId);
71 const ids = entries.map(entry => entry.id);
72
73 const { count } = await this.client.prisma.afkEntry.deleteMany({
74 where: {
75 id: {
76 in: ids
77 }
78 }
79 });
80
81 for (const key of entries.keys()) {
82 this.entries.delete(key);
83 }
84
85 return {
86 count,
87 entries
88 };
89 }
90
91 async removeAFK(
92 guildId: string,
93 userId: string,
94 shouldAwait: boolean = true,
95 failIfGuildEntryNotFound = false
96 ) {
97 if (failIfGuildEntryNotFound && !this.entries.has(`${guildId}_${userId}`)) {
98 return null;
99 }
100
101 const entry =
102 this.entries.get(`${guildId}_${userId}`) ?? this.entries.get(`global_${userId}`);
103
104 if (!entry) {
105 return null;
106 }
107
108 const promise = this.client.prisma.afkEntry.deleteMany({
109 where: {
110 id: entry.id
111 }
112 });
113
114 shouldAwait ? await promise : promise.then(log);
115
116 this.entries.delete(`${entry.global ? "global" : guildId}_${userId}`);
117 return entry;
118 }
119
120 async startAFK(guildId: string, userId: string, reason?: string, global: boolean = false) {
121 const entry = await this.client.prisma.afkEntry.create({
122 data: {
123 guildId,
124 userId,
125 reason,
126 global
127 }
128 });
129
130 this.entries.set(`${global ? "global" : guildId}_${userId}`, entry);
131 return entry;
132 }
133
134 addMentions(
135 guildId: string,
136 userId: string,
137 mentions: { userId: Snowflake; messageLink: string }[]
138 ) {
139 if (!this.isAFK(guildId, userId)) {
140 return false;
141 }
142
143 const entry = this.entries.get(`${guildId}_${userId}`)!;
144
145 if (!entry || entry.mentions.length >= 10) {
146 return false;
147 }
148
149 entry.mentions.push(
150 ...mentions.map(m => `${m.userId}__${m.messageLink}__${new Date().toISOString()}`)
151 );
152 this.modifiedIds.add(`${entry.global ? "global" : guildId}_${userId}`);
153 this.queueSync();
154
155 return true;
156 }
157
158 queueSync() {
159 this.syncTimeout ??= setTimeout(() => {
160 for (const guildId_userId of this.modifiedIds) {
161 const entry = this.entries.get(guildId_userId);
162
163 if (!entry) {
164 continue;
165 }
166
167 this.client.prisma.afkEntry.updateMany({
168 where: {
169 id: entry.id
170 },
171 data: entry
172 });
173 }
174 }, this.syncTimeoutDelay);
175 }
176
177 @GatewayEventListener("messageCreate")
178 async onMessageCreate(message: Message<boolean>) {
179 if (message.author.bot || !message.guild || message.channel.type === ChannelType.DM) {
180 return;
181 }
182
183 if (this.isAFK(message.guildId!, message.author.id)) {
184 const entry = await this.removeAFK(message.guildId!, message.author.id, false);
185
186 await message.reply({
187 embeds: [
188 {
189 color: 0x007bff,
190 description: this.client.afkService.generateAFKEndMessage(entry)
191 }
192 ],
193 allowedMentions: {
194 users: [],
195 repliedUser: false,
196 roles: []
197 }
198 });
199 }
200
201 if (message.mentions.users.size === 0) {
202 return;
203 }
204
205 let description = "";
206 const users = message.mentions.users.filter(user => this.isAFK(message.guildId!, user.id));
207
208 if (users.size === 0) {
209 return;
210 }
211
212 for (const [id] of users) {
213 this.addMentions(message.guildId!, id, [
214 {
215 messageLink: message.url,
216 userId: message.author.id
217 }
218 ]);
219 }
220
221 if (users.size === 1) {
222 const entry =
223 this.entries.get(`${message.guildId!}_${users.at(0)!.id}`) ??
224 this.entries.get(`global_${users.at(0)!.id}`);
225 description = `<@${users.at(0)!.id}> is AFK right now${
226 entry?.reason ? `, for reason: **${escapeMarkdown(entry?.reason)}**` : ""
227 }, for ${formatDistanceToNowStrict(entry?.createdAt ?? new Date())}`;
228 } else {
229 description = "The following users are AFK right now: \n\n";
230
231 for (const [id] of users) {
232 if (this.isAFK(message.guildId!, id)) {
233 const entry =
234 this.entries.get(`${message.guildId!}_${id}`) ??
235 this.entries.get(`global_${id}`);
236 description += `* <@${id}>: ${entry?.reason ?? "*No reason provided*"} ${
237 entry?.createdAt ? `(${time(entry.createdAt)})` : ""
238 } - ${time(entry?.createdAt ?? new Date(), "R")}\n`;
239 }
240 }
241 }
242
243 if (description.trim() === "") {
244 return;
245 }
246
247 message
248 .reply({
249 embeds: [
250 {
251 color: 0x007bff,
252 description
253 }
254 ],
255 allowedMentions: {
256 users: [],
257 repliedUser: false,
258 roles: []
259 }
260 })
261 .catch(logError);
262 }
263
264 @GatewayEventListener("guildMemberRemove")
265 onGuildMemberRemove(member: GuildMember) {
266 if (this.isAFK(member.guild.id, member.user.id)) {
267 this.removeAFK(member.guild.id, member.user.id).catch(logError);
268 }
269 }
270
271 generateAFKEndMessage(entry: AfkEntry | null | undefined) {
272 return (
273 `You were AFK for ${formatDistanceToNowStrict(entry?.createdAt ?? new Date())}. You had **${
274 entry?.mentions.length ?? 0
275 }** mentions in this server.` +
276 (entry?.mentions?.length
277 ? "\n\n" +
278 entry.mentions
279 .map(data => {
280 const [userId, messageLink, dateISO] = data.split("__");
281 return `From <@${userId}>, ${formatDistanceToNowStrict(
282 new Date(dateISO),
283 {
284 addSuffix: true
285 }
286 )} [Navigate](${messageLink})`;
287 })
288 .join("\n")
289 : "")
290 );
291 }
292
293 get(key: `${Snowflake | "global"}_${Snowflake}`) {
294 return this.entries.get(key);
295 }
296
297 getEntries() {
298 return this.entries;
299 }
300 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26