From 1d42d922bdda1f7eddd7a819655b8b663a7c67af Mon Sep 17 00:00:00 2001 From: diced Date: Tue, 23 Aug 2022 09:38:29 -0700 Subject: [PATCH] feat: discord webhook notifs --- src/lib/config/Config.ts | 25 ++++++ src/lib/config/readConfig.ts | 23 +++++- src/lib/config/validateConfig.ts | 24 +++++- src/lib/discord.ts | 132 +++++++++++++++++++++++++++++++ src/pages/api/shorten.ts | 8 +- src/pages/api/upload.ts | 5 ++ 6 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/lib/discord.ts diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index a0ed048..1240ac1 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -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; } \ No newline at end of file diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index 41ab7c5..a4f4bb5 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -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; } \ No newline at end of file diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 3632f03..2e9dda4 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -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; diff --git a/src/lib/discord.ts b/src/lib/discord.ts new file mode 100644 index 0000000..6583c6a --- /dev/null +++ b/src/lib/discord.ts @@ -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; +} \ No newline at end of file diff --git a/src/pages/api/shorten.ts b/src/pages/api/shorten.ts index e736b9a..15c0e80 100644 --- a/src/pages/api/shorten.ts +++ b/src/pages/api/shorten.ts @@ -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}` }); } diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index ad68298..097686e 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -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) {