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

Annotation of /branches/7.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: 8791 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 axios from "axios";
21     import chalk from "chalk";
22     import { spawnSync } from "child_process";
23     import { formatDistanceToNowStrict } from "date-fns";
24     import { APIEmbed, ActivityType, Colors, WebhookClient, escapeCodeBlock } from "discord.js";
25     import figlet from "figlet";
26     import { existsSync, readFileSync } from "fs";
27     import { rm } from "fs/promises";
28     import path from "path";
29     import { gt } from "semver";
30     import { version } from "../../package.json";
31     import Service from "../core/Service";
32     import { GatewayEventListener } from "../decorators/GatewayEventListener";
33     import { HasEventListeners } from "../types/HasEventListeners";
34     import { safeChannelFetch, safeMessageFetch } from "../utils/fetch";
35     import { log, logError, logInfo, logSuccess } from "../utils/logger";
36     import { chunkedString, getEmoji, sudoPrefix } from "../utils/utils";
37    
38     export const name = "startupManager";
39    
40     const { BACKUP_CHANNEL_ID, ERROR_WEKHOOK_URL } = process.env;
41    
42     export default class StartupManager extends Service implements HasEventListeners {
43     interval: NodeJS.Timer | undefined = undefined;
44     readonly packageJsonUrl = "https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/package.json";
45    
46     @GatewayEventListener("ready")
47     async onReady() {
48     if (BACKUP_CHANNEL_ID) {
49     this.setBackupQueue();
50     }
51    
52     if (ERROR_WEKHOOK_URL) {
53     log("Error webhook URL found. Setting up error handlers...");
54     this.setupErrorHandlers();
55     }
56    
57     const restartJsonFile = path.join(sudoPrefix("tmp", true), "restart.json");
58    
59     if (existsSync(restartJsonFile)) {
60     logInfo("Found restart.json file: ", restartJsonFile);
61    
62     try {
63     const { guildId, messageId, channelId, time } = JSON.parse(readFileSync(restartJsonFile, { encoding: "utf-8" }));
64    
65     const guild = this.client.guilds.cache.get(guildId);
66    
67     if (!guild) {
68     return;
69     }
70    
71     const channel = await safeChannelFetch(guild, channelId);
72    
73     if (!channel || !channel.isTextBased()) {
74     return;
75     }
76    
77     const message = await safeMessageFetch(channel, messageId);
78    
79     if (!message) {
80     return;
81     }
82    
83     await message.edit({
84     embeds: [
85     {
86     color: Colors.Green,
87     title: "System Restart",
88     description: `${getEmoji(this.client, "check")} Operation completed. (took ${(
89     (Date.now() - time) /
90     1000
91     ).toFixed(2)}s)`
92     }
93     ]
94     });
95     } catch (e) {
96     logError(e);
97     }
98    
99     rm(restartJsonFile).catch(logError);
100     }
101    
102     const { presence } = this.client.configManager.systemConfig;
103    
104     this.client.user?.setPresence({
105     activities: [
106     {
107     name: presence?.name ?? "over the server",
108     type: ActivityType[presence?.type ?? "Watching"],
109     url: presence?.url
110     }
111     ],
112     status: presence?.status ?? "dnd"
113     });
114     }
115    
116     async sendErrorLog(content: string) {
117     const url = ERROR_WEKHOOK_URL;
118    
119     if (!url) {
120     return;
121     }
122    
123     const client = new WebhookClient({
124     url
125     });
126     const chunks = chunkedString(content, 4000);
127     const embeds: APIEmbed[] = [
128     {
129     title: "Fatal error",
130     color: 0xf14a60,
131     description: "```" + escapeCodeBlock(chunks[0]) + "```"
132     }
133     ];
134    
135     if (chunks.length > 1) {
136     for (let i = 1; i < chunks.length; i++) {
137     embeds.push({
138     color: 0xf14a60,
139     description: "```" + escapeCodeBlock(chunks[i]) + "```",
140     timestamp: i === chunks.length - 1 ? new Date().toISOString() : undefined
141     });
142     }
143     } else {
144     embeds[0].timestamp = new Date().toISOString();
145     }
146    
147     await client
148     .send({
149     embeds
150     })
151     .catch(logError);
152     }
153    
154     setupErrorHandlers() {
155     process.on("unhandledRejection", (reason: unknown) => {
156     process.removeAllListeners("unhandledRejection");
157     logError(reason);
158     this.sendErrorLog(
159     `Unhandled promise rejection: ${
160     typeof reason === "string" || typeof (reason as any)?.toString === "function"
161     ? escapeCodeBlock((reason as any)?.toString ? (reason as any).toString() : (reason as any))
162     : reason
163     }`
164     ).finally(() => process.exit(-1));
165     });
166    
167     process.on("uncaughtException", async (error: Error) => {
168     process.removeAllListeners("uncaughtException");
169     logError(error);
170     this.sendErrorLog(
171     error.stack ?? `Uncaught ${error.name.trim() === "" ? "Error" : error.name}: ${error.message}`
172     ).finally(() => process.exit(-1));
173     });
174     }
175    
176     async sendConfigBackupCopy() {
177     if (!BACKUP_CHANNEL_ID) {
178     return;
179     }
180    
181     const channel = this.client.channels.cache.get(BACKUP_CHANNEL_ID);
182    
183     if (!channel?.isTextBased()) {
184     return;
185     }
186    
187     await channel
188     ?.send({
189     content: "# Configuration Backup",
190     files: [this.client.configManager.configPath, this.client.configManager.systemConfigPath]
191     })
192     .catch(logError);
193     }
194    
195     setBackupQueue() {
196     const time = process.env.BACKUP_INTERVAL ? parseInt(process.env.BACKUP_INTERVAL) : 1000 * 60 * 60 * 2;
197     const finalTime = isNaN(time) ? 1000 * 60 * 60 * 2 : time;
198     this.interval = setInterval(this.sendConfigBackupCopy.bind(this), finalTime);
199     logInfo(`Configuration backups will be sent in each ${formatDistanceToNowStrict(new Date(Date.now() - finalTime))}`);
200     logInfo(`Sending initial backup`);
201     this.sendConfigBackupCopy();
202     }
203    
204     systemUpdate(branch = "main") {
205     if (spawnSync(`git pull origin ${branch}`).error?.message.endsWith("ENOENT")) {
206     logError("Cannot perform an automatic update - the system does not have Git installed and available in $PATH.");
207     return false;
208     }
209    
210     if (spawnSync(`npm run build`).error) {
211     logError("Cannot perform an automatic update - failed to build the project");
212     return false;
213     }
214    
215     const { version } = require("../../package.json");
216     logSuccess(`Successfully completed automatic update - system upgraded to version ${version}`);
217     return true;
218     }
219    
220     async checkForUpdate() {
221     try {
222     const response = await axios.get(this.packageJsonUrl);
223     const newVersion = response.data?.version;
224    
225     if (typeof newVersion === "string" && gt(newVersion, this.client.metadata.data.version)) {
226     logInfo("Found update - performing an automatic update");
227     this.systemUpdate();
228     }
229     } catch (e) {
230     logError(e);
231     }
232     }
233    
234     boot() {
235     return new Promise<void>((resolve, reject) => {
236     figlet.text(
237     "SudoBot",
238     {
239     font: "Standard"
240     },
241     (error, data) => {
242     if (error) {
243     reject(error);
244     return;
245     }
246    
247     console.info(chalk.blueBright(data));
248     console.info(`Version ${chalk.green(version)} -- Booting up`);
249     resolve();
250     }
251     );
252     });
253     }
254     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26