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 { Snowflake } from "discord.js"; |
21 |
|
|
import fs, { writeFile } from "fs/promises"; |
22 |
|
|
import path from "path"; |
23 |
|
|
import { AnyZodObject, z } from "zod"; |
24 |
|
|
import { zodToJsonSchema } from "zod-to-json-schema"; |
25 |
|
|
import type { Extension } from "../core/Extension"; |
26 |
|
|
import Service from "../core/Service"; |
27 |
|
|
import { GuildConfig, GuildConfigSchema } from "../types/GuildConfigSchema"; |
28 |
|
|
import { SystemConfig, SystemConfigSchema } from "../types/SystemConfigSchema"; |
29 |
|
|
import { log, logDebug, logInfo } from "../utils/Logger"; |
30 |
|
|
import { sudoPrefix } from "../utils/utils"; |
31 |
|
|
|
32 |
|
|
export * from "../types/GuildConfigSchema"; |
33 |
|
|
|
34 |
|
|
export const name = "configManager"; |
35 |
|
|
|
36 |
|
|
export type GuildConfigContainer = { |
37 |
|
|
[key: string]: GuildConfig | undefined; |
38 |
|
|
}; |
39 |
|
|
|
40 |
|
|
export default class ConfigManager extends Service { |
41 |
|
|
public readonly configPath = sudoPrefix("config/config.json"); |
42 |
|
|
public readonly systemConfigPath = sudoPrefix("config/system.json"); |
43 |
|
|
public readonly schemaDirectory = sudoPrefix("config/schema", true); |
44 |
|
|
public readonly configSchemaPath = path.join(this.schemaDirectory, "config.json"); |
45 |
|
|
public readonly systemConfigSchemaPath = path.join(this.schemaDirectory, "system.json"); |
46 |
|
|
|
47 |
|
|
protected configSchemaInfo = |
48 |
|
|
"https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/config/schema/config.json"; |
49 |
|
|
protected systemConfigSchemaInfo = |
50 |
|
|
"https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/config/schema/system.json"; |
51 |
|
|
protected loaded = false; |
52 |
|
|
protected guildConfigSchema = GuildConfigSchema; |
53 |
|
|
protected systemConfigSchema = SystemConfigSchema; |
54 |
|
|
protected guildConfigContainerSchema = this.guildConfigContainer(); |
55 |
|
|
|
56 |
|
|
config: GuildConfigContainer = {} as GuildConfigContainer; |
57 |
|
|
systemConfig: SystemConfig = {} as SystemConfig; |
58 |
|
|
|
59 |
|
|
/** |
60 |
|
|
* This service is manually booted by the Extension Service. |
61 |
|
|
*/ |
62 |
|
|
async manualBoot() { |
63 |
|
|
await this.loadOnce(); |
64 |
|
|
} |
65 |
|
|
|
66 |
|
|
private guildConfigContainer() { |
67 |
|
|
return z.record(z.string(), this.guildConfigSchema.optional().or(z.undefined())); |
68 |
|
|
} |
69 |
|
|
|
70 |
|
|
loadOnce() { |
71 |
|
|
if (this.loaded) { |
72 |
|
|
return; |
73 |
|
|
} |
74 |
|
|
|
75 |
|
|
this.loaded = true; |
76 |
|
|
return this.load(); |
77 |
|
|
} |
78 |
|
|
|
79 |
|
|
async load() { |
80 |
|
|
log(`Loading system configuration from file: ${this.systemConfigPath}`); |
81 |
|
|
const systemConfigFileContents = await fs.readFile(this.systemConfigPath, { |
82 |
|
|
encoding: "utf-8" |
83 |
|
|
}); |
84 |
|
|
|
85 |
|
|
log(`Loading guild configuration from file: ${this.configPath}`); |
86 |
|
|
const configFileContents = await fs.readFile(this.configPath, { encoding: "utf-8" }); |
87 |
|
|
|
88 |
|
|
const configJSON = JSON.parse(configFileContents); |
89 |
|
|
const systemConfigJSON = JSON.parse(systemConfigFileContents); |
90 |
|
|
|
91 |
|
|
if ("$schema" in configJSON) { |
92 |
|
|
this.configSchemaInfo = configJSON.$schema; |
93 |
|
|
delete configJSON.$schema; |
94 |
|
|
} |
95 |
|
|
|
96 |
|
|
if ("$schema" in systemConfigJSON) { |
97 |
|
|
this.systemConfigSchemaInfo = systemConfigJSON.$schema; |
98 |
|
|
delete systemConfigJSON.$schema; |
99 |
|
|
} |
100 |
|
|
|
101 |
|
|
this.config = this.guildConfigContainerSchema.parse(configJSON); |
102 |
|
|
this.systemConfig = this.systemConfigSchema.parse(systemConfigJSON); |
103 |
|
|
logInfo("Successfully loaded the configuration files"); |
104 |
|
|
} |
105 |
|
|
|
106 |
|
|
onReady() { |
107 |
|
|
const guildIds = this.client.guilds.cache.keys(); |
108 |
|
|
|
109 |
|
|
if (!process.env.PRIVATE_BOT_MODE) { |
110 |
|
|
for (const id of guildIds) { |
111 |
|
|
if (id in this.config) { |
112 |
|
|
continue; |
113 |
|
|
} |
114 |
|
|
|
115 |
|
|
logInfo(`Auto configuring default settings for guild: ${id}`); |
116 |
|
|
this.autoConfigure(id); |
117 |
|
|
} |
118 |
|
|
} |
119 |
|
|
|
120 |
|
|
if (!process.env.NO_GENERATE_CONFIG_SCHEMA) { |
121 |
|
|
this.client.logger.info("Generating configuration schema files"); |
122 |
|
|
this.generateSchema(); |
123 |
|
|
} |
124 |
|
|
} |
125 |
|
|
|
126 |
|
|
autoConfigure(id: Snowflake) { |
127 |
|
|
this.config[id] = this.guildConfigSchema.parse({}); |
128 |
|
|
} |
129 |
|
|
|
130 |
|
|
testConfig() { |
131 |
|
|
const guildResult = this.guildConfigContainerSchema.safeParse(this.config); |
132 |
|
|
|
133 |
|
|
if (!guildResult.success) { |
134 |
|
|
return { error: guildResult.error, type: "guild" as const }; |
135 |
|
|
} |
136 |
|
|
|
137 |
|
|
const systemResult = this.systemConfigSchema.safeParse(this.systemConfig); |
138 |
|
|
|
139 |
|
|
if (!systemResult.success) { |
140 |
|
|
return { error: systemResult.error, type: "system" as const }; |
141 |
|
|
} |
142 |
|
|
|
143 |
|
|
return null; |
144 |
|
|
} |
145 |
|
|
|
146 |
|
|
async write({ guild = true, system = true } = {}) { |
147 |
|
|
if (guild) { |
148 |
|
|
log(`Writing guild configuration to file: ${this.configPath}`); |
149 |
|
|
|
150 |
|
|
const json = JSON.stringify( |
151 |
|
|
{ |
152 |
|
|
$schema: this.configSchemaInfo, |
153 |
|
|
...this.config |
154 |
|
|
}, |
155 |
|
|
null, |
156 |
|
|
4 |
157 |
|
|
); |
158 |
|
|
|
159 |
|
|
await writeFile(this.configPath, json, { encoding: "utf-8" }); |
160 |
|
|
} |
161 |
|
|
|
162 |
|
|
if (system) { |
163 |
|
|
log(`Writing system configuration to file: ${this.systemConfigPath}`); |
164 |
|
|
|
165 |
|
|
const json = JSON.stringify( |
166 |
|
|
{ |
167 |
|
|
$schema: this.systemConfigSchemaInfo, |
168 |
|
|
...this.systemConfig |
169 |
|
|
}, |
170 |
|
|
null, |
171 |
|
|
4 |
172 |
|
|
); |
173 |
|
|
|
174 |
|
|
await writeFile(this.systemConfigPath, json, { encoding: "utf-8" }); |
175 |
|
|
} |
176 |
|
|
|
177 |
|
|
logInfo("Successfully wrote the configuration files"); |
178 |
|
|
} |
179 |
|
|
|
180 |
|
|
get<T extends GuildConfig = GuildConfig>(guildId: Snowflake): T | undefined { |
181 |
|
|
return this.config[guildId] as T | undefined; |
182 |
|
|
} |
183 |
|
|
|
184 |
|
|
set(guildId: Snowflake, value: GuildConfig) { |
185 |
|
|
this.config[guildId] = value; |
186 |
|
|
} |
187 |
|
|
|
188 |
|
|
async registerExtensionConfig(extensions: Extension[]) { |
189 |
|
|
if (extensions.length === 0) { |
190 |
|
|
return; |
191 |
|
|
} |
192 |
|
|
|
193 |
|
|
logDebug("Registering extension configuration schemas"); |
194 |
|
|
|
195 |
|
|
let finalGuildConfigSchema: AnyZodObject = this.guildConfigSchema; |
196 |
|
|
let finalSystemConfigSchema: AnyZodObject = this.systemConfigSchema; |
197 |
|
|
|
198 |
|
|
for (const extension of extensions) { |
199 |
|
|
const guildConfigSchema = await extension.guildConfig(); |
200 |
|
|
const systemConfigSchema = await extension.systemConfig(); |
201 |
|
|
|
202 |
|
|
if (guildConfigSchema) { |
203 |
|
|
finalGuildConfigSchema = finalGuildConfigSchema.extend(guildConfigSchema); |
204 |
|
|
} |
205 |
|
|
|
206 |
|
|
if (systemConfigSchema) { |
207 |
|
|
finalSystemConfigSchema = finalSystemConfigSchema.extend(systemConfigSchema); |
208 |
|
|
} |
209 |
|
|
} |
210 |
|
|
|
211 |
|
|
this.systemConfigSchema = finalSystemConfigSchema as typeof this.systemConfigSchema; |
212 |
|
|
this.guildConfigSchema = finalGuildConfigSchema as typeof this.guildConfigSchema; |
213 |
|
|
this.guildConfigContainerSchema = this.guildConfigContainer(); |
214 |
|
|
} |
215 |
|
|
|
216 |
|
|
async generateSchema() { |
217 |
|
|
const configSchema = JSON.stringify( |
218 |
|
|
zodToJsonSchema(this.guildConfigContainerSchema), |
219 |
|
|
null, |
220 |
|
|
4 |
221 |
|
|
); |
222 |
|
|
await writeFile(this.configSchemaPath, configSchema, { encoding: "utf-8" }); |
223 |
|
|
logInfo("Successfully generated the guild configuration schema file"); |
224 |
|
|
|
225 |
|
|
const systemConfigSchema = JSON.stringify( |
226 |
|
|
zodToJsonSchema(this.systemConfigSchema), |
227 |
|
|
null, |
228 |
|
|
4 |
229 |
|
|
); |
230 |
|
|
await writeFile(this.systemConfigSchemaPath, systemConfigSchema, { encoding: "utf-8" }); |
231 |
|
|
logInfo("Successfully generated the system configuration schema file"); |
232 |
|
|
} |
233 |
|
|
} |