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

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26