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

Annotation of /branches/8.x/src/services/StartupManager.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 10802 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
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 archiver from "archiver";
21     import axios from "axios";
22     import chalk from "chalk";
23     import { spawnSync } from "child_process";
24     import { formatDistanceToNowStrict } from "date-fns";
25     import {
26     APIEmbed,
27     ActivityType,
28     Attachment,
29     AttachmentBuilder,
30     Colors,
31     WebhookClient,
32     escapeCodeBlock
33     } from "discord.js";
34     import figlet from "figlet";
35     import { existsSync, readFileSync } from "fs";
36     import { rm } from "fs/promises";
37     import path from "path";
38     import { gt } from "semver";
39     import { version } from "../../package.json";
40     import Service from "../core/Service";
41     import { HasEventListeners } from "../types/HasEventListeners";
42     import { log, logError, logInfo, logSuccess } from "../utils/Logger";
43     import { safeChannelFetch, safeMessageFetch } from "../utils/fetch";
44     import { chunkedString, getEmoji, sudoPrefix } from "../utils/utils";
45    
46     export const name = "startupManager";
47    
48     const { BACKUP_CHANNEL_ID, ERROR_WEBHOOK_URL, BACKUP_STORAGE } = process.env;
49    
50     export default class StartupManager extends Service implements HasEventListeners {
51     interval: Timer | undefined = undefined;
52     readonly packageJsonUrl =
53     "https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/package.json";
54    
55     async onReady() {
56     if (BACKUP_CHANNEL_ID) {
57     this.setBackupQueue();
58     }
59    
60     if (ERROR_WEBHOOK_URL) {
61     log("Error webhook URL found. Setting up error handlers...");
62     this.setupErrorHandlers();
63     }
64    
65     const restartJsonFile = path.join(sudoPrefix("tmp", true), "restart.json");
66    
67     if (existsSync(restartJsonFile)) {
68     logInfo("Found restart.json file: ", restartJsonFile);
69    
70     try {
71     const { guildId, messageId, channelId, time } = JSON.parse(
72     readFileSync(restartJsonFile, { encoding: "utf-8" })
73     );
74    
75     const guild = this.client.guilds.cache.get(guildId);
76    
77     if (!guild) {
78     return;
79     }
80    
81     const channel = await safeChannelFetch(guild, channelId);
82    
83     if (!channel || !channel.isTextBased()) {
84     return;
85     }
86    
87     const message = await safeMessageFetch(channel, messageId);
88    
89     if (!message) {
90     return;
91     }
92    
93     await message.edit({
94     embeds: [
95     {
96     color: Colors.Green,
97     title: "System Restart",
98     description: `${getEmoji(
99     this.client,
100     "check"
101     )} Operation completed. (took ${((Date.now() - time) / 1000).toFixed(
102     2
103     )}s)`
104     }
105     ]
106     });
107     } catch (e) {
108     logError(e);
109     }
110    
111     rm(restartJsonFile).catch(logError);
112     }
113    
114     const { presence } = this.client.configManager.systemConfig;
115    
116     this.client.user?.setPresence({
117     activities: [
118     {
119     name: presence?.name ?? "over the server",
120     type: ActivityType[presence?.type ?? "Watching"],
121     url: presence?.url
122     }
123     ],
124     status: presence?.status ?? "dnd"
125     });
126     }
127    
128     async sendErrorLog(content: string) {
129     const url = ERROR_WEBHOOK_URL;
130    
131     if (!url) {
132     return;
133     }
134    
135     const client = new WebhookClient({
136     url
137     });
138     const chunks = chunkedString(content, 4000);
139     const embeds: APIEmbed[] = [
140     {
141     title: "Fatal error",
142     color: 0xf14a60,
143     description: "```" + escapeCodeBlock(chunks[0]) + "```"
144     }
145     ];
146    
147     if (chunks.length > 1) {
148     for (let i = 1; i < chunks.length; i++) {
149     embeds.push({
150     color: 0xf14a60,
151     description: "```" + escapeCodeBlock(chunks[i]) + "```",
152     timestamp: i === chunks.length - 1 ? new Date().toISOString() : undefined
153     });
154     }
155     } else {
156     embeds[0].timestamp = new Date().toISOString();
157     }
158    
159     await client
160     .send({
161     embeds
162     })
163     .catch(logError);
164     }
165    
166     setupErrorHandlers() {
167     process.on("unhandledRejection", (reason: unknown) => {
168     process.removeAllListeners("unhandledRejection");
169     logError(reason);
170     this.sendErrorLog(
171     `Unhandled promise rejection: ${
172     typeof reason === "string" ||
173     typeof (reason as string | undefined)?.toString === "function"
174     ? escapeCodeBlock(
175     (reason as string | undefined)?.toString
176     ? (reason as string).toString()
177     : (reason as string)
178     )
179     : reason
180     }`
181     ).finally(() => process.exit(-1));
182     });
183    
184     process.on("uncaughtException", async (error: Error) => {
185     process.removeAllListeners("uncaughtException");
186     logError(error);
187     this.sendErrorLog(
188     error.stack ??
189     `Uncaught ${error.name.trim() === "" ? "Error" : error.name}: ${error.message}`
190     ).finally(() => process.exit(-1));
191     });
192     }
193    
194     async sendConfigBackupCopy() {
195     if (!BACKUP_CHANNEL_ID) {
196     return;
197     }
198    
199     const channel = this.client.channels.cache.get(BACKUP_CHANNEL_ID);
200    
201     if (!channel?.isTextBased()) {
202     return;
203     }
204    
205     const files: Array<string | AttachmentBuilder | Attachment> = [
206     this.client.configManager.configPath,
207     this.client.configManager.systemConfigPath
208     ];
209    
210     if (BACKUP_STORAGE) {
211     if (process.isBun) {
212     logError("Cannot create storage backup in a Bun environment");
213     return;
214     }
215    
216     const buffer = await this.makeStorageBackup();
217    
218     // check for discord max attachment size limit
219     if (buffer.byteLength > 80 * 1024 * 1024) {
220     logError("Storage backup is too large to send to Discord");
221     return;
222     }
223    
224     files.push(
225     new AttachmentBuilder(buffer, {
226     name: "storage.zip"
227     })
228     );
229    
230     logInfo("Storage backup created");
231     }
232    
233     await channel
234     ?.send({
235     content: "# Configuration Backup",
236     files
237     })
238     .catch(logError);
239     }
240    
241     makeStorageBackup() {
242     return new Promise<Buffer>((resolve, reject) => {
243     const archive = archiver("zip", {
244     zlib: { level: 9 }
245     });
246    
247     const bufferList: Buffer[] = [];
248    
249     archive.on("data", data => {
250     bufferList.push(data);
251     });
252    
253     archive.on("end", () => {
254     const resultBuffer = Buffer.concat(bufferList);
255     resolve(resultBuffer);
256     });
257    
258     archive.on("error", err => {
259     reject(err);
260     });
261    
262     archive.directory(sudoPrefix("storage", true), false);
263     archive.finalize();
264     });
265     }
266    
267     setBackupQueue() {
268     const time = process.env.BACKUP_INTERVAL
269     ? parseInt(process.env.BACKUP_INTERVAL)
270     : 1000 * 60 * 60 * 2;
271     const finalTime = isNaN(time) ? 1000 * 60 * 60 * 2 : time;
272     this.interval = setInterval(this.sendConfigBackupCopy.bind(this), finalTime);
273     logInfo(
274     `Configuration backups will be sent in each ${formatDistanceToNowStrict(
275     new Date(Date.now() - finalTime)
276     )}`
277     );
278     logInfo("Sending initial backup");
279     this.sendConfigBackupCopy();
280     }
281    
282     systemUpdate(branch = "main") {
283     if (spawnSync(`git pull origin ${branch}`).error?.message.endsWith("ENOENT")) {
284     logError(
285     "Cannot perform an automatic update - the system does not have Git installed and available in $PATH."
286     );
287     return false;
288     }
289    
290     if (spawnSync("npm run build").error) {
291     logError("Cannot perform an automatic update - failed to build the project");
292     return false;
293     }
294    
295     const { version } = require("../../package.json");
296     logSuccess(
297     `Successfully completed automatic update - system upgraded to version ${version}`
298     );
299     return true;
300     }
301    
302     async checkForUpdate() {
303     try {
304     const response = await axios.get(this.packageJsonUrl);
305     const newVersion = response.data?.version;
306    
307     if (
308     typeof newVersion === "string" &&
309     gt(newVersion, this.client.metadata.data.version)
310     ) {
311     logInfo("Found update - performing an automatic update");
312     this.systemUpdate();
313     }
314     } catch (e) {
315     logError(e);
316     }
317     }
318    
319     boot() {
320     axios.defaults.headers.common["Accept-Encoding"] = "gzip";
321     return new Promise<void>((resolve, reject) => {
322     figlet.text(
323     "SudoBot",
324     {
325     font: "Standard"
326     },
327     (error, data) => {
328     if (error) {
329     reject(error);
330     return;
331     }
332    
333     console.info(chalk.blueBright(data));
334     console.info(`Version ${chalk.green(version)} -- Booting up`);
335     resolve();
336     }
337     );
338     });
339     }
340     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26