3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00
zeppelin/backend/src/api/guilds/importExport.ts
Tiago R 06877e90cc
Update djs & knub (#395)
* 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>
2023-04-01 14:58:17 +03:00

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);
}