/[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 370 - (show annotations)
Mon Jul 29 17:29:50 2024 UTC (8 months, 2 weeks ago) by rakin
File MIME type: application/typescript
File size: 8206 byte(s)
fix(api): config corruption while specifying arrays
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
11 export default class ConfigController extends Controller {
12 globalMiddleware(): Function[] {
13 return [RequireAuth, ValidatorError];
14 }
15
16 middleware(): KeyValuePair<Function[]> {
17 return {
18 update: [
19 body(["config"]).isObject()
20 ]
21 };
22 }
23
24 private zodSchema() {
25 const snowflake = zod.string().regex(/\d+/, { message: "The given value is not a Snowflake" });
26
27 const schema = zod.object({
28 "prefix": zod.string().optional(),
29 "debug": zod.boolean().optional(),
30 "mute_role": snowflake.optional(),
31 "gen_role": snowflake.optional(),
32 "logging_channel": snowflake.optional(),
33 "logging_channel_join_leave": snowflake.optional(),
34 "mod_role": snowflake.optional(),
35 "announcement_channel": snowflake.optional(),
36 "admin": snowflake.optional(),
37 "lockall": zod.array(zod.string()).optional(),
38 "warn_notallowed": zod.boolean().optional(),
39 "role_commands": zod.record(
40 snowflake,
41 zod.array(zod.string().min(1))
42 ).optional(),
43 "autoclear": zod.object({
44 "enabled": zod.boolean().optional(),
45 "channels": zod.array(snowflake).optional()
46 }).optional(),
47 "verification": zod.object({
48 "enabled": zod.boolean().optional(),
49 "role": snowflake.optional()
50 }).optional(),
51 "welcomer": zod.object({
52 "enabled": zod.boolean().optional(),
53 "channel": snowflake.optional(),
54 "message": zod.string().min(1).or(zod.null()).optional(),
55 "randomize": zod.boolean().optional()
56 }).optional(),
57 "cooldown": zod.object({
58 "enabled": zod.boolean().optional(),
59 "global": zod.any().optional(),
60 "cmds": zod.object({}).optional()
61 }).optional(),
62 "starboard": zod.object({
63 "enabled": zod.boolean().optional(),
64 "reactions": zod.number().int().optional(),
65 "channel": snowflake.optional()
66 }).optional(),
67 "autorole": zod.object({
68 "enabled": zod.boolean().optional(),
69 "roles": zod.array(snowflake).optional()
70 }).optional(),
71 "spam_filter": zod.object({
72 "enabled": zod.boolean().optional(),
73 "limit": zod.number().int().optional(),
74 "time": zod.number().optional(),
75 "diff": zod.number().optional(),
76 "exclude": zod.array(snowflake).optional(),
77 "samelimit": zod.number().int().optional(),
78 "unmute_in": zod.number().optional()
79 }).optional(),
80 "raid": zod.object({
81 "enabled": zod.boolean().optional(),
82 "max_joins": zod.number().int().optional(),
83 "time": zod.number().optional(),
84 "channels": zod.array(snowflake).optional(),
85 "exclude": zod.boolean().optional()
86 }).optional(),
87 "global_commands": zod.array(zod.string()).optional(),
88 "filters": zod.object({
89 "ignore_staff": zod.boolean().optional(),
90 "chars_repeated": zod.number().int().optional(),
91 "words_repeated": zod.number().int().optional(),
92 "words": zod.array(zod.string()).optional(),
93 "tokens": zod.array(zod.string()).optional(),
94 "invite_message": zod.string().optional(),
95 "words_excluded": zod.array(snowflake).optional(),
96 "domain_excluded": zod.array(snowflake).optional(),
97 "invite_excluded": zod.array(snowflake).optional(),
98 "words_enabled": zod.boolean().optional(),
99 "invite_enabled": zod.boolean().optional(),
100 "domain_enabled": zod.boolean().optional(),
101 "regex": zod.boolean().optional(),
102 "file_mimes_excluded": zod.array(zod.string()).optional(),
103 "file_types_excluded": zod.array(zod.string()).optional(),
104 "domains": zod.array(zod.string()).optional(),
105 "regex_patterns": zod.array(zod.string()).optional(),
106 "rickrolls_enabled": zod.boolean().optional(),
107 "pings": zod.number().int().optional()
108 }).optional()
109 });
110
111 return schema;
112 }
113
114 public async index(request: Request) {
115 const { id } = request.params;
116
117 if (!request.user?.guilds.includes(id)) {
118 return this.response({ error: "You don't have permission to access configuration of this guild." }, 403);
119 }
120
121 if (id === "global" || !this.client.config.props[id]) {
122 return this.response({ error: "No configuration found for the given guild ID" }, 404);
123 }
124
125 return this.client.config.props[id];
126 }
127
128 public async update(request: Request) {
129 const { id } = request.params;
130 const { config } = request.body;
131
132 console.log(config);
133
134 try {
135 const currentConfigDotObject = dot(this.client.config.props[id]);
136 let newConfigDotObject = {...currentConfigDotObject};
137
138 console.log("Input: ", config);
139
140 const result = this.zodSchema().safeParse(object({...config}));
141
142 if (!result?.success) {
143 return this.response({ error: "The data schema does not match.", error_type: 'validation', errors: result.error.errors }, 422);
144 }
145
146 for (const configKey in config) {
147 const regexMatched = /(.+)\[\d+\]/g.test(configKey);
148
149 if (typeof currentConfigDotObject[configKey] === 'undefined' && !regexMatched) {
150 return this.response({ error: `The key '${configKey}' is not allowed` }, 422);
151 }
152
153 if (!regexMatched && config[configKey] !== null && config[configKey] !== null && typeof config[configKey] !== typeof currentConfigDotObject[configKey]) {
154 console.log(typeof config[configKey], typeof currentConfigDotObject[configKey]);
155
156 if (typeof currentConfigDotObject[configKey] === 'number' && typeof config[configKey] === 'string') {
157 const int = parseInt(config[configKey]);
158
159 if (int !== NaN) {
160 newConfigDotObject[configKey] = int;
161 console.log("Updating: ", configKey, config[configKey], newConfigDotObject[configKey]);
162 continue;
163 }
164 }
165
166 return this.response({ error: `The key '${configKey}' has incompatible value type '${config[configKey] === null ? 'null' : typeof config[configKey]}'` }, 422);
167 }
168
169 console.log("Updating: ", configKey, config[configKey], newConfigDotObject[configKey]);
170 }
171
172
173 newConfigDotObject = merge.withOptions({ mergeArrays: false }, object({...newConfigDotObject}), object({...config}));
174 console.log("Output: ", newConfigDotObject);
175
176 this.client.config.props[id] = newConfigDotObject;
177 this.client.config.write();
178
179 return { message: "Configuration updated", previous: currentConfigDotObject, new: dot(this.client.config.props[id]) };
180 }
181 catch (e) {
182 console.log(e);
183 return this.response({ error: 'Internal Server Error', error_type: 'internal' }, 500);
184 }
185 }
186 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26