mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00

* update pkgs
Signed-off-by: GitHub <noreply@github.com>
* new knub typings
Signed-off-by: GitHub <noreply@github.com>
* more pkg updates
Signed-off-by: GitHub <noreply@github.com>
* more fixes
Signed-off-by: GitHub <noreply@github.com>
* channel typings
Signed-off-by: GitHub <noreply@github.com>
* more message utils typings fixes
Signed-off-by: GitHub <noreply@github.com>
* migrate permissions
Signed-off-by: GitHub <noreply@github.com>
* fix: InternalPoster webhookables
Signed-off-by: GitHub <noreply@github.com>
* djs typings: Attachment & Util
Signed-off-by: GitHub <noreply@github.com>
* more typings
Signed-off-by: GitHub <noreply@github.com>
* fix: rename permissionNames
Signed-off-by: GitHub <noreply@github.com>
* more fixes
Signed-off-by: GitHub <noreply@github.com>
* half the number of errors
* knub commands => messageCommands
Signed-off-by: GitHub <noreply@github.com>
* configPreprocessor => configParser
Signed-off-by: GitHub <noreply@github.com>
* fix channel.messages
Signed-off-by: GitHub <noreply@github.com>
* revert automod any typing
Signed-off-by: GitHub <noreply@github.com>
* more configParser typings
Signed-off-by: GitHub <noreply@github.com>
* revert
Signed-off-by: GitHub <noreply@github.com>
* remove knub type params
Signed-off-by: GitHub <noreply@github.com>
* fix more MessageEmbed / MessageOptions
Signed-off-by: GitHub <noreply@github.com>
* dumb commit for @almeidx to see why this is stupid
Signed-off-by: GitHub <noreply@github.com>
* temp disable custom_events
Signed-off-by: GitHub <noreply@github.com>
* more minor typings fixes - 23 err left
Signed-off-by: GitHub <noreply@github.com>
* update djs dep
* +debug build method (revert this)
Signed-off-by: GitHub <noreply@github.com>
* Revert "+debug build method (revert this)"
This reverts commit a80af1e729
.
* Redo +debug build (Revert this)
Signed-off-by: GitHub <noreply@github.com>
* uniform before/after Load shorthands
Signed-off-by: GitHub <noreply@github.com>
* remove unused imports & add prettier plugin
Signed-off-by: GitHub <noreply@github.com>
* env fixes for web platform hosting
Signed-off-by: GitHub <noreply@github.com>
* feat: knub v32-next; related fixes
* fix: allow legacy keys in change_perms action
* fix: request Message Content intent
* fix: use Knub's config validation logic in API
* fix(dashboard): fix error when there are no message and/or slash commands in a plugin
* fix(automod): start_thread action thread options
* fix(CustomEvents): message command types
* chore: remove unneeded type annotation
* feat: add forum channel icon; use thread icon for news threads
* chore: make tslint happy
* chore: fix formatting
---------
Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: almeidx <almeidx@pm.me>
Co-authored-by: Dragory <2606411+Dragory@users.noreply.github.com>
193 lines
6 KiB
TypeScript
193 lines
6 KiB
TypeScript
import { ApiPermissions } from "@shared/apiPermissions";
|
|
import express, { Request, Response } from "express";
|
|
import moment from "moment-timezone";
|
|
import { z } from "zod";
|
|
import { Case } from "../../data/entities/Case";
|
|
import { GuildCases } from "../../data/GuildCases";
|
|
import { MINUTES } from "../../utils";
|
|
import { requireGuildPermission } from "../permissions";
|
|
import { rateLimit } from "../rateLimits";
|
|
import { clientError, ok } from "../responses";
|
|
|
|
const caseHandlingModeSchema = z.union([
|
|
z.literal("replace"),
|
|
z.literal("bumpExistingCases"),
|
|
z.literal("bumpImportedCases"),
|
|
]);
|
|
|
|
type CaseHandlingMode = z.infer<typeof caseHandlingModeSchema>;
|
|
|
|
const caseNoteData = z.object({
|
|
mod_id: z.string(),
|
|
mod_name: z.string(),
|
|
body: z.string(),
|
|
created_at: z.string(),
|
|
});
|
|
|
|
const caseData = z.object({
|
|
case_number: z.number(),
|
|
user_id: z.string(),
|
|
user_name: z.string(),
|
|
mod_id: z.nullable(z.string()),
|
|
mod_name: z.nullable(z.string()),
|
|
type: z.number(),
|
|
created_at: z.string(),
|
|
is_hidden: z.boolean(),
|
|
pp_id: z.nullable(z.string()),
|
|
pp_name: z.nullable(z.string()),
|
|
|
|
notes: z.array(caseNoteData),
|
|
});
|
|
|
|
const importExportData = z.object({
|
|
cases: z.array(caseData),
|
|
});
|
|
type TImportExportData = z.infer<typeof importExportData>;
|
|
|
|
export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
|
const importExportRouter = express.Router();
|
|
|
|
importExportRouter.get(
|
|
"/:guildId/pre-import",
|
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
|
async (req: Request, res: Response) => {
|
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
|
const minNum = await guildCases.getMinCaseNumber();
|
|
const maxNum = await guildCases.getMaxCaseNumber();
|
|
|
|
return {
|
|
minCaseNumber: minNum,
|
|
maxCaseNumber: maxNum,
|
|
};
|
|
},
|
|
);
|
|
|
|
importExportRouter.post(
|
|
"/:guildId/import",
|
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
|
rateLimit(
|
|
(req) => `import-${req.params.guildId}`,
|
|
5 * MINUTES,
|
|
"A single server can only import data once every 5 minutes",
|
|
),
|
|
async (req: Request, res: Response) => {
|
|
let data: TImportExportData;
|
|
try {
|
|
data = importExportData.parse(req.body.data);
|
|
} catch (err) {
|
|
const prettyMessage = `${err.issues[0].code}: expected ${err.issues[0].expected}, received ${
|
|
err.issues[0].received
|
|
} at /${err.issues[0].path.join("/")}`;
|
|
return clientError(res, `Invalid import data format: ${prettyMessage}`);
|
|
return;
|
|
}
|
|
|
|
let caseHandlingMode: CaseHandlingMode;
|
|
try {
|
|
caseHandlingMode = caseHandlingModeSchema.parse(req.body.caseHandlingMode);
|
|
} catch (err) {
|
|
return clientError(res, "Invalid case handling mode");
|
|
return;
|
|
}
|
|
|
|
const seenCaseNumbers = new Set();
|
|
for (const theCase of data.cases) {
|
|
if (seenCaseNumbers.has(theCase.case_number)) {
|
|
return clientError(res, `Duplicate case number: ${theCase.case_number}`);
|
|
}
|
|
seenCaseNumbers.add(theCase.case_number);
|
|
}
|
|
|
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
|
|
|
// Prepare cases
|
|
if (caseHandlingMode === "replace") {
|
|
// Replace existing cases
|
|
await guildCases.deleteAllCases();
|
|
} else if (caseHandlingMode === "bumpExistingCases") {
|
|
// Bump existing numbers
|
|
const maxNumberInData = data.cases.reduce((max, theCase) => Math.max(max, theCase.case_number), 0);
|
|
await guildCases.bumpCaseNumbers(maxNumberInData);
|
|
} else if (caseHandlingMode === "bumpImportedCases") {
|
|
const maxExistingNumber = await guildCases.getMaxCaseNumber();
|
|
for (const theCase of data.cases) {
|
|
theCase.case_number += maxExistingNumber;
|
|
}
|
|
}
|
|
|
|
// Import cases
|
|
for (const theCase of data.cases) {
|
|
const insertData: any = {
|
|
...theCase,
|
|
is_hidden: theCase.is_hidden ? 1 : 0,
|
|
guild_id: req.params.guildId,
|
|
notes: undefined,
|
|
};
|
|
|
|
const caseInsertData = await guildCases.createInternal(insertData);
|
|
for (const note of theCase.notes) {
|
|
await guildCases.createNote(caseInsertData.identifiers[0].id, note);
|
|
}
|
|
}
|
|
|
|
ok(res);
|
|
},
|
|
);
|
|
|
|
const exportBatchSize = 500;
|
|
importExportRouter.post(
|
|
"/:guildId/export",
|
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
|
rateLimit(
|
|
(req) => `export-${req.params.guildId}`,
|
|
5 * MINUTES,
|
|
"A single server can only export data once every 5 minutes",
|
|
),
|
|
async (req: Request, res: Response) => {
|
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
|
|
|
const data: TImportExportData = {
|
|
cases: [],
|
|
};
|
|
|
|
let n = 0;
|
|
let cases: Case[];
|
|
do {
|
|
cases = await guildCases.getExportCases(n, exportBatchSize);
|
|
n += cases.length;
|
|
|
|
for (const theCase of cases) {
|
|
data.cases.push({
|
|
case_number: theCase.case_number,
|
|
user_id: theCase.user_id,
|
|
user_name: theCase.user_name,
|
|
mod_id: theCase.mod_id,
|
|
mod_name: theCase.mod_name,
|
|
type: theCase.type,
|
|
created_at: theCase.created_at,
|
|
is_hidden: theCase.is_hidden,
|
|
pp_id: theCase.pp_id,
|
|
pp_name: theCase.pp_name,
|
|
|
|
notes: theCase.notes.map((note) => ({
|
|
mod_id: note.mod_id,
|
|
mod_name: note.mod_name,
|
|
body: note.body,
|
|
created_at: note.created_at,
|
|
})),
|
|
});
|
|
}
|
|
} while (cases.length === exportBatchSize);
|
|
|
|
const filename = `export_${req.params.guildId}_${moment().format("YYYY-MM-DD_HH-mm-ss")}.json`;
|
|
const serialized = JSON.stringify(data, null, 2);
|
|
|
|
res.setHeader("Content-Disposition", `attachment; filename=${filename}`);
|
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
res.setHeader("Content-Length", serialized.length);
|
|
res.send(serialized);
|
|
},
|
|
);
|
|
|
|
guildRouter.use("/", importExportRouter);
|
|
}
|