/[sudobot]/trunk/src/api/controllers/ConfigController.ts
ViewVC logotype

Contents of /trunk/src/api/controllers/ConfigController.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 379 - (show annotations)
Mon Jul 29 17:29:52 2024 UTC (8 months, 1 week ago) by rakin
File MIME type: application/typescript
File size: 11047 byte(s)
fix(api): config input not merging properly
1 import { dot, object } from "dot-object";
2 import { body } from "express-validator";
3 import { z as zod, ZodSchema } from "zod";
4 import KeyValuePair from "../../types/KeyValuePair";
5 import Controller from "../Controller";
6 import RequireAuth from "../middleware/RequireAuth";
7 import ValidatorError from "../middleware/ValidatorError";
8 import Request from "../Request";
9 import merge from 'ts-deepmerge';
10 import { config } from "../../client/Config";
11
12 export default class ConfigController extends Controller {
13 globalMiddleware(): Function[] {
14 return [RequireAuth, ValidatorError];
15 }
16
17 middleware(): KeyValuePair<Function[]> {
18 return {
19 update: [
20 body(["config"]).isObject()
21 ]
22 };
23 }
24
25 private zodSchema(id: string) {
26 const snowflake = zod.string().regex(/\d+/, { message: "The given value is not a Snowflake" });
27 const config = this.client.config.props[id];
28
29 const schema = zod.object({
30 "prefix": zod.string().optional(),
31 "debug": zod.boolean().optional(),
32 "mute_role": snowflake.optional(),
33 "gen_role": snowflake.optional(),
34 "logging_channel": snowflake.optional(),
35 "logging_channel_join_leave": snowflake.optional(),
36 "mod_role": snowflake.optional(),
37 "announcement_channel": snowflake.optional(),
38 "admin": snowflake.optional(),
39 "lockall": zod.array(zod.string()).optional(),
40 "warn_notallowed": zod.boolean().optional(),
41 "role_commands": zod.record(
42 snowflake,
43 zod.array(zod.string().min(1))
44 ).optional().default({}),
45 "autoclear": zod.object({
46 "enabled": zod.boolean().optional(),
47 "channels": zod.array(snowflake).optional().default(config.autoclear.channels)
48 }).optional(),
49 "verification": zod.object({
50 "enabled": zod.boolean().optional(),
51 "role": snowflake.optional()
52 }).optional(),
53 "welcomer": zod.object({
54 "enabled": zod.boolean().optional(),
55 "channel": snowflake.optional(),
56 "message": zod.string().min(1).or(zod.null()).optional(),
57 "randomize": zod.boolean().optional()
58 }).optional(),
59 "cooldown": zod.object({
60 "enabled": zod.boolean().optional(),
61 "global": zod.any().optional(),
62 "cmds": zod.object({}).optional()
63 }).optional(),
64 "starboard": zod.object({
65 "enabled": zod.boolean().optional(),
66 "reactions": zod.number().int().optional(),
67 "channel": snowflake.optional()
68 }).optional(),
69 "autorole": zod.object({
70 "enabled": zod.boolean().optional(),
71 "roles": zod.array(snowflake).optional().default(config.autorole.roles)
72 }).optional(),
73 "spam_filter": zod.object({
74 "enabled": zod.boolean().optional(),
75 "limit": zod.number().int().optional(),
76 "time": zod.number().optional(),
77 "diff": zod.number().optional(),
78 "exclude": zod.array(snowflake).optional().default(config.spam_filter.exclude),
79 "samelimit": zod.number().int().optional(),
80 "unmute_in": zod.number().optional()
81 }).optional(),
82 "raid": zod.object({
83 "enabled": zod.boolean().optional(),
84 "max_joins": zod.number().int().optional(),
85 "time": zod.number().optional(),
86 "channels": zod.array(snowflake).optional().default(config.raid.channels),
87 "exclude": zod.boolean().optional()
88 }).optional(),
89 "global_commands": zod.array(zod.string()).optional().default(config.global_commands),
90 "filters": zod.object({
91 "ignore_staff": zod.boolean().optional(),
92 "chars_repeated": zod.number().int().optional(),
93 "words_repeated": zod.number().int().optional(),
94 "words": zod.array(zod.string()).optional().default(config.filters.words),
95 "tokens": zod.array(zod.string()).optional().default(config.filters.tokens),
96 "invite_message": zod.string().optional(),
97 "words_excluded": zod.array(snowflake).optional().default(config.filters.words_excluded),
98 "domain_excluded": zod.array(snowflake).optional().default(config.filters.domain_excluded),
99 "invite_excluded": zod.array(snowflake).optional().default(config.filters.invite_excluded),
100 "words_enabled": zod.boolean().optional(),
101 "invite_enabled": zod.boolean().optional(),
102 "domain_enabled": zod.boolean().optional(),
103 "regex": zod.boolean().optional(),
104 "file_mimes_excluded": zod.array(zod.string()).optional().default(config.filters.file_mimes_excluded),
105 "file_types_excluded": zod.array(zod.string()).optional().default(config.filters.file_types_excluded),
106 "domains": zod.array(zod.string()).optional().default(config.filters.domains),
107 "regex_patterns": zod.array(zod.string()).optional().default(config.filters.regex_patterns),
108 "rickrolls_enabled": zod.boolean().optional(),
109 "pings": zod.number().int().optional()
110 }).optional()
111 });
112
113 return schema;
114 }
115
116 public async index(request: Request) {
117 const { id } = request.params;
118
119 if (!request.user?.guilds.includes(id)) {
120 return this.response({ error: "You don't have permission to access configuration of this guild." }, 403);
121 }
122
123 if (id === "global" || !this.client.config.props[id]) {
124 return this.response({ error: "No configuration found for the given guild ID" }, 404);
125 }
126
127 return this.client.config.props[id];
128 }
129
130 public async update(request: Request) {
131 const { id } = request.params;
132 const { config: origconfig } = request.body;
133
134 console.log(origconfig);
135
136 try {
137 const currentConfigDotObject = this.client.config.props[id];
138
139 console.log("Current config: ", currentConfigDotObject);
140
141 const result = this.zodSchema(id).safeParse(object(origconfig));
142
143 if (!result?.success) {
144 return this.response({ error: "The data schema does not match.", error_type: 'validation', errors: result.error.errors }, 422);
145 }
146
147 const config = result.data;
148 const newConfig = merge.withOptions({ mergeArrays: false }, currentConfigDotObject, config);
149 const result2 = this.zodSchema(id).safeParse(newConfig);
150
151 if (!result2?.success) {
152 console.log(result2.error.errors);
153 return this.response({ error: 'Internal Server Error (500)', error_type: 'internal' }, 500);
154 }
155
156 console.log('Final', newConfig);
157 this.client.config.props[id] = newConfig;
158 this.client.config.write();
159 return { message: "Configuration updated", previous: dot(currentConfigDotObject), new: dot(this.client.config.props[id]) };
160 }
161 catch (e) {
162 console.log(e);
163 return this.response({ error: 'Internal Server Error', error_type: 'internal' }, 500);
164 }
165 }
166
167 public async update2(request: Request) {
168 const { id } = request.params;
169 const { config: origconfig } = request.body;
170
171 console.log(origconfig);
172
173 try {
174 const currentConfigDotObject = dot(this.client.config.props[id]);
175 let newConfigDotObject = {...currentConfigDotObject};
176
177 console.log("Current config: ", currentConfigDotObject);
178
179 const result = this.zodSchema(id).safeParse(object({...origconfig}));
180
181 if (!result?.success) {
182 return this.response({ error: "The data schema does not match.", error_type: 'validation', errors: result.error.errors }, 422);
183 }
184
185 const config = result.data;
186
187 console.log(config);
188
189 for (const key in config) {
190 const configKey = key as keyof typeof config;
191 const regexMatched = /(.+)\[\d+\]/g.test(configKey);
192
193 if (typeof currentConfigDotObject[configKey] === 'undefined' && !regexMatched) {
194 console.log(configKey, config[configKey]);
195
196 if (currentConfigDotObject[configKey] instanceof Array && config[configKey] instanceof Array) {
197 console.log('Array');
198 }
199 else
200 return this.response({ error: `The key '${configKey}' is not allowed` }, 422);
201 }
202
203 if (!regexMatched && config[configKey] !== null && config[configKey] !== null && typeof config[configKey] !== typeof currentConfigDotObject[configKey]) {
204 console.log(typeof config[configKey], typeof currentConfigDotObject[configKey]);
205
206 if (typeof currentConfigDotObject[configKey] === 'number' && typeof config[configKey] === 'string') {
207 const int = parseInt(config[configKey]!.toString());
208
209 if (int !== NaN) {
210 newConfigDotObject[configKey] = int;
211 console.log("Updating: ", configKey, config[configKey], newConfigDotObject[configKey]);
212 continue;
213 }
214 }
215
216 return this.response({ error: `The key '${configKey}' has incompatible value type '${config[configKey] === null ? 'null' : typeof config[configKey]}'` }, 422);
217 }
218
219 console.log("Updating: ", configKey, config[configKey], newConfigDotObject[configKey]);
220 }
221
222 const newObj = object({...newConfigDotObject});
223 const configObj = object({...config});
224
225 // console.log("Newobj", newObj);
226 // console.log("Configobj", configObj);
227
228 newConfigDotObject = merge.withOptions({ mergeArrays: false }, newObj, configObj);
229 console.log("Output: ", newConfigDotObject);
230
231 this.client.config.props[id] = newConfigDotObject;
232 this.client.config.write();
233
234 return { message: "Configuration updated", previous: currentConfigDotObject, new: dot(this.client.config.props[id]) };
235 }
236 catch (e) {
237 console.log(e);
238 return this.response({ error: 'Internal Server Error', error_type: 'internal' }, 500);
239 }
240 }
241 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26