From ca766bb1d8b0e1d9c247980f71aab2455cc4f3fa Mon Sep 17 00:00:00 2001 From: diced Date: Thu, 13 Jun 2024 22:27:13 -0700 Subject: [PATCH] feat: http webhooks --- src/lib/api/upload/partialUpload.ts | 14 ++-- src/lib/api/upload/upload.ts | 26 +++---- src/lib/config/read.ts | 10 +++ src/lib/config/safe.ts | 7 +- src/lib/config/validate.ts | 4 ++ src/lib/{ => webhooks}/discord.ts | 18 ++--- src/lib/webhooks/http.ts | 87 ++++++++++++++++++++++++ src/lib/webhooks/index.ts | 14 ++++ src/offload/partial.ts | 2 +- src/server/routes/api/user/urls/index.ts | 2 +- 10 files changed, 151 insertions(+), 33 deletions(-) rename src/lib/{ => webhooks}/discord.ts (93%) create mode 100644 src/lib/webhooks/http.ts create mode 100644 src/lib/webhooks/index.ts diff --git a/src/lib/api/upload/partialUpload.ts b/src/lib/api/upload/partialUpload.ts index 7eea4ae6..340a3032 100755 --- a/src/lib/api/upload/partialUpload.ts +++ b/src/lib/api/upload/partialUpload.ts @@ -3,13 +3,13 @@ import { FastifyRequest } from 'fastify'; import { writeFile } from 'fs/promises'; import { extname, join } from 'path'; import { Worker } from 'worker_threads'; -import { config } from '../../config'; -import { hashPassword } from '../../crypto'; -import { prisma } from '../../db'; -import { log } from '../../logger'; -import { guess } from '../../mimes'; -import { formatFileName } from '../../uploader/formatFileName'; -import { UploadHeaders, UploadOptions } from '../../uploader/parseHeaders'; +import { config } from '@/lib/config'; +import { hashPassword } from '@/lib/crypto'; +import { prisma } from '@/lib/db'; +import { log } from '@/lib/logger'; +import { guess } from '@/lib/mimes'; +import { formatFileName } from '@/lib/uploader/formatFileName'; +import { UploadHeaders, UploadOptions } from '@/lib/uploader/parseHeaders'; const logger = log('api').c('upload'); export async function handlePartialUpload({ diff --git a/src/lib/api/upload/upload.ts b/src/lib/api/upload/upload.ts index 9e3bc3ab..bcbe366e 100755 --- a/src/lib/api/upload/upload.ts +++ b/src/lib/api/upload/upload.ts @@ -1,19 +1,19 @@ import { ApiUploadResponse, MultipartFileBuffer } from '@/server/routes/api/upload'; import { FastifyRequest } from 'fastify'; import { extname } from 'path'; -import { bytes } from '../../bytes'; -import { compress } from '../../compress'; -import { config } from '../../config'; -import { hashPassword } from '../../crypto'; -import { datasource } from '../../datasource'; -import { prisma } from '../../db'; -import { fileSelect } from '../../db/models/file'; -import { onUpload } from '../../discord'; -import { removeGps } from '../../gps'; -import { log } from '../../logger'; -import { guess } from '../../mimes'; -import { formatFileName } from '../../uploader/formatFileName'; -import { UploadHeaders, UploadOptions } from '../../uploader/parseHeaders'; +import { bytes } from '@/lib/bytes'; +import { compress } from '@/lib/compress'; +import { config } from '@/lib/config'; +import { hashPassword } from '@/lib/crypto'; +import { datasource } from '@/lib/datasource'; +import { prisma } from '@/lib/db'; +import { fileSelect } from '@/lib/db/models/file'; +import { onUpload } from '@/lib/webhooks'; +import { removeGps } from '@/lib/gps'; +import { log } from '@/lib/logger'; +import { guess } from '@/lib/mimes'; +import { formatFileName } from '@/lib/uploader/formatFileName'; +import { UploadHeaders, UploadOptions } from '@/lib/uploader/parseHeaders'; const logger = log('api').c('upload'); diff --git a/src/lib/config/read.ts b/src/lib/config/read.ts index 248f8b96..a7fa9b43 100755 --- a/src/lib/config/read.ts +++ b/src/lib/config/read.ts @@ -117,6 +117,10 @@ export const rawConfig: any = { adminBypass: undefined, allowList: undefined, }, + httpWebhook: { + onUpload: undefined, + onShorten: undefined, + }, }; export const PROP_TO_ENV = { @@ -239,6 +243,9 @@ export const PROP_TO_ENV = { 'ratelimit.window': 'RATELIMIT_WINDOW', 'ratelimit.adminBypass': 'RATELIMIT_ADMIN_BYPASS', 'ratelimit.allowList': 'RATELIMIT_ALLOW_LIST', + + 'httpWebhook.onUpload': 'HTTP_WEBHOOK_ONUPLOAD', + 'httpWebhook.onShorten': 'HTTP_WEBHOOK_ONSHORTEN', }; const logger = log('config').c('read'); @@ -359,6 +366,9 @@ export function readEnv() { env('ratelimit.window', 'ms'), env('ratelimit.adminBypass', 'boolean'), env('ratelimit.allowList', 'string[]'), + + env('httpWebhook.onUpload', 'string'), + env('httpWebhook.onShorten', 'string'), ]; // clone raw diff --git a/src/lib/config/safe.ts b/src/lib/config/safe.ts index 618af034..e0da0f64 100755 --- a/src/lib/config/safe.ts +++ b/src/lib/config/safe.ts @@ -2,7 +2,10 @@ import { config } from '.'; import enabled from '../oauth/enabled'; import { Config } from './validate'; -export type SafeConfig = Omit & { +export type SafeConfig = Omit< + Config, + 'oauth' | 'datasource' | 'core' | 'discord' | 'httpWebhook' | 'ratelimit' +> & { oauthEnabled: ReturnType; oauth: { bypassLocalLogin: boolean; @@ -11,7 +14,7 @@ export type SafeConfig = Omit & { }; export function safeConfig(): SafeConfig { - const { datasource: _d, core: _c, oauth, ...rest } = config; + const { datasource: _d, core: _c, oauth, discord: _di, ratelimit: _r, httpWebhook: _h, ...rest } = config; (rest as SafeConfig).oauthEnabled = enabled(config); (rest as SafeConfig).oauth = { diff --git a/src/lib/config/validate.ts b/src/lib/config/validate.ts index c2776a82..8328bbeb 100755 --- a/src/lib/config/validate.ts +++ b/src/lib/config/validate.ts @@ -275,6 +275,10 @@ export const schema = z.object({ adminBypass: z.boolean().default(true), allowList: z.array(z.string()).default([]), }), + httpWebhook: z.object({ + onUpload: z.string().url().nullable().default(null), + onShorten: z.string().url().nullable().default(null), + }), }); export type Config = z.infer; diff --git a/src/lib/discord.ts b/src/lib/webhooks/discord.ts similarity index 93% rename from src/lib/discord.ts rename to src/lib/webhooks/discord.ts index 3ebc943e..f2f47ff3 100755 --- a/src/lib/discord.ts +++ b/src/lib/webhooks/discord.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; -import { discordContent } from './config/validate'; -import { ParseValue, parseString } from './parser'; -import { config } from './config'; -import { File } from './db/models/file'; -import { User } from './db/models/user'; -import { log } from './logger'; -import { Url } from './db/models/url'; -import { parserMetrics } from './parser/metrics'; +import { discordContent } from '../config/validate'; +import { ParseValue, parseString } from '../parser'; +import { config } from '../config'; +import { File } from '../db/models/file'; +import { User } from '../db/models/user'; +import { log } from '../logger'; +import { Url } from '../db/models/url'; +import { parserMetrics } from '../parser/metrics'; -const logger = log('discord'); +const logger = log('webhooks').c('discord'); export type DiscordContent = z.infer; export type WebhooksExecuteBody = { diff --git a/src/lib/webhooks/http.ts b/src/lib/webhooks/http.ts new file mode 100644 index 00000000..6eb7de3b --- /dev/null +++ b/src/lib/webhooks/http.ts @@ -0,0 +1,87 @@ +import { config } from '../config'; +import { log } from '../logger'; +import { onUpload as discordOnUpload, onShorten as discordOnShorten } from './discord'; + +const logger = log('webhooks').c('http'); + +export async function onUpload({ user, file, link }: Parameters[0]) { + if (!config.httpWebhook.onUpload) return; + if (!URL.canParse(config.httpWebhook.onUpload)) { + logger.debug('invalid url for http onUpload'); + return; + } + + delete (user).oauthProviders; + delete user.passkeys; + delete user.token; + delete user.password; + delete user.totpSecret; + delete (file).password; + + const payload = { + type: 'upload', + data: { + user, + file, + link, + }, + }; + + const res = await fetch(config.httpWebhook.onUpload, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + 'x-zipline-webhook': 'true', + 'x-zipline-webhook-type': 'upload', + }, + }); + + if (!res.ok) { + const text = await res.text(); + logger.error('webhook failed', { response: text, status: res.status }); + } + + return; +} + +export async function onShorten({ user, url, link }: Parameters[0]) { + if (!config.httpWebhook.onShorten) return; + if (!URL.canParse(config.httpWebhook.onShorten)) { + logger.debug('invalid url for http onShorten'); + return; + } + + delete (user).oauthProviders; + delete user.passkeys; + delete user.token; + delete user.password; + delete user.totpSecret; + delete (url).password; + + const payload = { + type: 'shorten', + data: { + user, + url, + link, + }, + }; + + const res = await fetch(config.httpWebhook.onShorten, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + 'x-zipline-webhook': 'true', + 'x-zipline-webhook-type': 'shorten', + }, + }); + + if (!res.ok) { + const text = await res.text(); + logger.error('webhook failed', { response: text, status: res.status }); + } + + return; +} diff --git a/src/lib/webhooks/index.ts b/src/lib/webhooks/index.ts new file mode 100644 index 00000000..cd4f63b9 --- /dev/null +++ b/src/lib/webhooks/index.ts @@ -0,0 +1,14 @@ +import { onUpload as discordOnUpload, onShorten as discordOnShorten } from './discord'; +import { onUpload as httpOnUpload, onShorten as httpOnShorten } from './http'; + +export async function onUpload(args: Parameters[0]) { + Promise.all([discordOnUpload(args), httpOnUpload(args)]); + + return; +} + +export async function onShorten(args: Parameters[0]) { + Promise.all([discordOnShorten(args), httpOnShorten(args)]); + + return; +} diff --git a/src/offload/partial.ts b/src/offload/partial.ts index a0ee5bbf..8ef6bec8 100755 --- a/src/offload/partial.ts +++ b/src/offload/partial.ts @@ -3,7 +3,7 @@ import { config } from '@/lib/config'; import { prisma } from '@/lib/db'; import { fileSelect } from '@/lib/db/models/file'; import { userSelect } from '@/lib/db/models/user'; -import { onUpload } from '@/lib/discord'; +import { onUpload } from '@/lib/webhooks'; import { log } from '@/lib/logger'; import { UploadOptions } from '@/lib/uploader/parseHeaders'; import { open, readFile, readdir, rm } from 'fs/promises'; diff --git a/src/server/routes/api/user/urls/index.ts b/src/server/routes/api/user/urls/index.ts index cc95caeb..e837913b 100755 --- a/src/server/routes/api/user/urls/index.ts +++ b/src/server/routes/api/user/urls/index.ts @@ -5,7 +5,7 @@ import { Url } from '@/lib/db/models/url'; import { log } from '@/lib/logger'; import { z } from 'zod'; import { Prisma } from '@prisma/client'; -import { onShorten } from '@/lib/discord'; +import { onShorten } from '@/lib/webhooks'; import fastifyPlugin from 'fastify-plugin'; import { userMiddleware } from '@/server/middleware/user';