feat: discord webhook notifs
This commit is contained in:
parent
4f631fbd0e
commit
1d42d922bd
6 changed files with 214 additions and 3 deletions
|
@ -112,6 +112,30 @@ export interface ConfigWebsite {
|
|||
show_files_per_user: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigDiscord {
|
||||
url: string;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
|
||||
upload: ConfigDiscordContent;
|
||||
shorten: ConfigDiscordContent;
|
||||
}
|
||||
|
||||
export interface ConfigDiscordContent {
|
||||
content: string;
|
||||
embed: ConfigDiscordEmbed;
|
||||
}
|
||||
|
||||
export interface ConfigDiscordEmbed {
|
||||
title?: string;
|
||||
description?: string;
|
||||
footer?: string;
|
||||
color?: number;
|
||||
thumbnail?: boolean;
|
||||
timestamp: boolean;
|
||||
image: boolean;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
core: ConfigCore;
|
||||
uploader: ConfigUploader;
|
||||
|
@ -119,4 +143,5 @@ export interface Config {
|
|||
ratelimit: ConfigRatelimit;
|
||||
datasource: ConfigDatasource;
|
||||
website: ConfigWebsite;
|
||||
discord: ConfigDiscord;
|
||||
}
|
|
@ -83,6 +83,28 @@ export default function readConfig() {
|
|||
|
||||
map('WEBSITE_TITLE', 'string', 'website.title'),
|
||||
map('WEBSITE_SHOW_FILES_PER_USER', 'boolean', 'website.show_files_per_user'),
|
||||
|
||||
map('DISCORD_URL', 'string', 'discord.url'),
|
||||
map('DISCORD_USERNAME', 'string', 'discord.username'),
|
||||
map('DISCORD_AVATAR_URL', 'string', 'discord.avatar_url'),
|
||||
|
||||
map('DISCORD_UPLOAD_CONTENT', 'string', 'discord.upload.content'),
|
||||
map('DISCORD_UPLOAD_EMBED_TITLE', 'string', 'discord.upload.embed.title'),
|
||||
map('DISCORD_UPLOAD_EMBED_DESCRIPTION', 'string', 'discord.upload.embed.description'),
|
||||
map('DISCORD_UPLOAD_EMBED_FOOTER', 'string', 'discord.upload.embed.footer'),
|
||||
map('DISCORD_UPLOAD_EMBED_COLOR', 'number', 'discord.upload.embed.color'),
|
||||
map('DISCORD_UPLOAD_EMBED_IMAGE', 'boolean', 'discord.upload.embed.image'),
|
||||
map('DISCORD_UPLOAD_EMBED_THUMBNAIL', 'boolean', 'discord.upload.embed.thumbnail'),
|
||||
map('DISCORD_UPLOAD_EMBED_TIMESTAMP', 'boolean', 'discord.upload.embed.timestamp'),
|
||||
|
||||
map('DISCORD_SHORTEN_CONTENT', 'string', 'discord.shorten.content'),
|
||||
map('DISCORD_SHORTEN_EMBED_TITLE', 'string', 'discord.shorten.embed.title'),
|
||||
map('DISCORD_SHORTEN_EMBED_DESCRIPTION', 'string', 'discord.shorten.embed.description'),
|
||||
map('DISCORD_SHORTEN_EMBED_FOOTER', 'string', 'discord.shorten.embed.footer'),
|
||||
map('DISCORD_SHORTEN_EMBED_COLOR', 'number', 'discord.shorten.embed.color'),
|
||||
map('DISCORD_SHORTEN_EMBED_IMAGE', 'boolean', 'discord.shorten.embed.image'),
|
||||
map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'),
|
||||
map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'),
|
||||
];
|
||||
|
||||
const config = {};
|
||||
|
@ -111,6 +133,5 @@ export default function readConfig() {
|
|||
set(config, map.path, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
|
@ -1,6 +1,19 @@
|
|||
import { Config } from 'lib/config/Config';
|
||||
import { object, bool, string, number, boolean, array } from 'yup';
|
||||
|
||||
const discord_content = object({
|
||||
content: string().nullable(),
|
||||
embed: object({
|
||||
title: string().nullable().default(null),
|
||||
description: string().nullable().default(null),
|
||||
footer: string().nullable().default(null),
|
||||
color: string().nullable().default(null),
|
||||
thumbnail: boolean().default(false),
|
||||
image: boolean().default(true),
|
||||
timestamp: boolean().default(true),
|
||||
}).nullable().default(null),
|
||||
}).nullable().default(null);
|
||||
|
||||
const validator = object({
|
||||
core: object({
|
||||
https: bool().default(false),
|
||||
|
@ -54,6 +67,13 @@ const validator = object({
|
|||
title: string().default('Zipline'),
|
||||
show_files_per_user: boolean().default(true),
|
||||
}),
|
||||
discord: object({
|
||||
url: string(),
|
||||
username: string().default('Zipline'),
|
||||
avatar_url: string().default('https://raw.githubusercontent.com/diced/zipline/9b60147e112ec5b70170500b85c75ea621f41d03/public/zipline.png'),
|
||||
upload: discord_content,
|
||||
shorten: discord_content,
|
||||
}).optional().nullable().default(null),
|
||||
});
|
||||
|
||||
export default function validate(config): Config {
|
||||
|
@ -89,7 +109,9 @@ export default function validate(config): Config {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(validated);
|
||||
|
||||
return validated as unknown as Config;
|
||||
} catch (e) {
|
||||
if (process.env.ZIPLINE_DOCKER_BUILD) return null;
|
||||
|
|
132
src/lib/discord.ts
Normal file
132
src/lib/discord.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { Image, Url, User } from '@prisma/client';
|
||||
import { ConfigDiscordContent } from 'lib/config/Config';
|
||||
import config from 'lib/config';
|
||||
import Logger from './logger';
|
||||
|
||||
// [user, image, url, route (ex. https://example.com/u/something.png)]
|
||||
export type Args = [User, Image?, Url?, string?];
|
||||
|
||||
function parse(str: string, args: Args) {
|
||||
if (!str) return null;
|
||||
|
||||
str = str
|
||||
.replace(/{user.admin}/gi, args[0].administrator ? 'yes' : 'no')
|
||||
.replace(/{user.id}/gi, args[0].id.toString())
|
||||
.replace(/{user.name}/gi, args[0].username)
|
||||
.replace(/{link}/gi, args[3]);
|
||||
|
||||
if (args[1]) str = str
|
||||
.replace(/{image.id}/gi, args[1].id.toString())
|
||||
.replace(/{image.mime}/gi, args[1].mimetype)
|
||||
.replace(/{image.file}/gi, args[1].file)
|
||||
.replace(/{image.created_at.full_string}/gi, args[1].created_at.toLocaleString())
|
||||
.replace(/{image.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString())
|
||||
.replace(/{image.created_at.date_string}/gi, args[1].created_at.toLocaleDateString());
|
||||
|
||||
if (args[2]) str = str
|
||||
.replace(/{url.id}/gi, args[2].id.toString())
|
||||
.replace(/{url.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
|
||||
.replace(/{url.destination}/gi, args[2].destination)
|
||||
.replace(/{url.created_at.full_string}/gi, args[2].created_at.toLocaleString())
|
||||
.replace(/{url.created_at.time_string}/gi, args[2].created_at.toLocaleTimeString())
|
||||
.replace(/{url.created_at.date_string}/gi, args[2].created_at.toLocaleDateString());
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export function parseContent(content: ConfigDiscordContent, args: Args): ConfigDiscordContent & { url: string } {
|
||||
return {
|
||||
content: parse(content.content, args),
|
||||
embed: content.embed ? {
|
||||
title: parse(content.embed.title, args),
|
||||
description: parse(content.embed.description, args),
|
||||
footer: parse(content.embed.footer, args),
|
||||
color: content.embed.color,
|
||||
thumbnail: content.embed.thumbnail,
|
||||
timestamp: content.embed.timestamp,
|
||||
image: content.embed.image,
|
||||
} : null,
|
||||
url: args[3],
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendUpload(user: User, image: Image, host: string) {
|
||||
if (!config.discord.upload) return;
|
||||
|
||||
const parsed = parseContent(config.discord.upload, [user, image, null, host]);
|
||||
const isImage = image.mimetype.startsWith('image/');
|
||||
|
||||
const body = {
|
||||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
embeds: parsed.embed ? [{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer ? {
|
||||
text: parsed.embed.footer,
|
||||
} : null,
|
||||
thumbnail: isImage && parsed.embed.thumbnail ? {
|
||||
url: parsed.url,
|
||||
} : null,
|
||||
image: isImage && parsed.embed.image ? {
|
||||
url: parsed.url,
|
||||
} : null,
|
||||
}] : null,
|
||||
};
|
||||
|
||||
const res = await fetch(config.discord.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
Logger.get('discord').error(`Failed to send upload notification to discord: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export async function sendShorten(user: User, url: Url, host: string) {
|
||||
if (!config.discord.shorten) return;
|
||||
|
||||
const parsed = parseContent(config.discord.shorten, [user, null, url, host]);
|
||||
|
||||
const body = {
|
||||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
embeds: parsed.embed ? [{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer ? {
|
||||
text: parsed.embed.footer,
|
||||
} : null,
|
||||
}] : null,
|
||||
};
|
||||
|
||||
console.log(body);
|
||||
|
||||
const res = await fetch(config.discord.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
Logger.get('discord').error(`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
|
@ -3,6 +3,8 @@ import zconfig from 'lib/config';
|
|||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvisURL, randomChars } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import { sendShorten } from 'lib/discord';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method !== 'POST') return res.forbid('no allow');
|
||||
|
@ -42,7 +44,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
if (req.headers.zws) invis = await createInvisURL(zconfig.urls.length, url.id);
|
||||
|
||||
Logger.get('url').info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
||||
Logger.get('url').info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
||||
|
||||
if (config.discord.shorten) {
|
||||
await sendShorten(user, url, `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id}`);
|
||||
}
|
||||
|
||||
return res.json({ url: `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id}` });
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { randomUUID } from 'crypto';
|
|||
import sharp from 'sharp';
|
||||
import { humanTime, parseExpiry } from 'lib/clientUtils';
|
||||
import { StringValue } from 'ms';
|
||||
import { sendUpload } from 'lib/discord';
|
||||
|
||||
const uploader = multer();
|
||||
|
||||
|
@ -127,6 +128,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
} else {
|
||||
response.files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
|
||||
if (zconfig.discord.upload) {
|
||||
await sendUpload(user, image, `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.administrator && zconfig.ratelimit.admin > 0) {
|
||||
|
|
Loading…
Add table
Reference in a new issue