From 91341e2d21b9fa5e856228ebdbc8157671dfbe49 Mon Sep 17 00:00:00 2001
From: diced <pranaco2@gmail.com>
Date: Sun, 11 Dec 2022 15:29:57 -0800
Subject: [PATCH] feat: new variables parser

---
 src/lib/discord.ts      |  62 ++++++-----------
 src/lib/utils/client.ts |  16 -----
 src/lib/utils/parser.ts | 143 ++++++++++++++++++++++++++++++++++++++++
 src/pages/api/upload.ts |  10 ++-
 src/pages/view/[id].tsx |  11 +++-
 src/server/index.ts     |  14 ++++
 6 files changed, 193 insertions(+), 63 deletions(-)
 create mode 100644 src/lib/utils/parser.ts

diff --git a/src/lib/discord.ts b/src/lib/discord.ts
index a41f868..de687b7 100644
--- a/src/lib/discord.ts
+++ b/src/lib/discord.ts
@@ -2,53 +2,21 @@ import { Image, Url, User } from '@prisma/client';
 import config from 'lib/config';
 import { ConfigDiscordContent } from 'lib/config/Config';
 import Logger from './logger';
-
-// [user, image, url, route (ex. https://example.com/r/something.png)]
-export type Args = [User, Image?, Url?, string?];
+import { parseString, ParseValue } from './utils/parser';
 
 const logger = Logger.get('discord');
 
-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(/{file\.id}/gi, args[1].id.toString())
-      .replace(/{file\.mime}/gi, args[1].mimetype)
-      .replace(/{file\.file}/gi, args[1].file)
-      .replace(/{file\.created_at.full_string}/gi, args[1].created_at.toLocaleString())
-      .replace(/{file\.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString())
-      .replace(/{file\.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
+  args: ParseValue
 ): ConfigDiscordContent & { url: string } {
   return {
-    content: parse(content.content, args),
+    content: parseString(content.content, args),
     embed: content.embed
       ? {
-          title: parse(content.embed.title, args),
-          description: parse(content.embed.description, args),
-          footer: parse(content.embed.footer, args),
+          title: parseString(content.embed.title, args),
+          description: parseString(content.embed.description, args),
+          footer: parseString(content.embed.footer, args),
           color: content.embed.color,
           thumbnail: content.embed.thumbnail,
           timestamp: content.embed.timestamp,
@@ -59,10 +27,16 @@ export function parseContent(
   };
 }
 
-export async function sendUpload(user: User, image: Image, host: string) {
+export async function sendUpload(user: User, image: Image, raw_link: string, link: string) {
   if (!config.discord.upload) return;
 
-  const parsed = parseContent(config.discord.upload, [user, image, null, host]);
+  const parsed = parseContent(config.discord.upload, {
+    file: image,
+    user,
+    link,
+    raw_link,
+  });
+
   const isImage = image.mimetype.startsWith('image/');
 
   const body = JSON.stringify({
@@ -118,10 +92,14 @@ export async function sendUpload(user: User, image: Image, host: string) {
   return;
 }
 
-export async function sendShorten(user: User, url: Url, host: string) {
+export async function sendShorten(user: User, url: Url, link: string) {
   if (!config.discord.shorten) return;
 
-  const parsed = parseContent(config.discord.shorten, [user, null, url, host]);
+  const parsed = parseContent(config.discord.shorten, {
+    url,
+    user,
+    link,
+  });
 
   const body = JSON.stringify({
     username: config.discord.username,
diff --git a/src/lib/utils/client.ts b/src/lib/utils/client.ts
index f504adc..ef0c59d 100644
--- a/src/lib/utils/client.ts
+++ b/src/lib/utils/client.ts
@@ -1,4 +1,3 @@
-import type { Image, User } from '@prisma/client';
 import dayjs from 'dayjs';
 import duration from 'dayjs/plugin/duration';
 import dayjsRelativeTime from 'dayjs/plugin/relativeTime';
@@ -6,21 +5,6 @@ import ms, { StringValue } from 'ms';
 dayjs.extend(duration);
 dayjs.extend(dayjsRelativeTime);
 
-export function parse(str: string, image: Image, user: User) {
-  if (!str) return null;
-
-  return str
-    .replace(/{user\.admin}/gi, user.administrator ? 'yes' : 'no')
-    .replace(/{user\.id}/gi, user.id.toString())
-    .replace(/{user\.name}/gi, user.username)
-    .replace(/{image\.id}/gi, image.id.toString())
-    .replace(/{image\.mime}/gi, image.mimetype)
-    .replace(/{image\.file}/gi, image.file)
-    .replace(/{image\.created_at.full_string}/gi, image.created_at.toLocaleString())
-    .replace(/{image\.created_at.time_string}/gi, image.created_at.toLocaleTimeString())
-    .replace(/{image\.created_at.date_string}/gi, image.created_at.toLocaleDateString());
-}
-
 export function randomChars(length: number) {
   const charset = 'QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890';
 
diff --git a/src/lib/utils/parser.ts b/src/lib/utils/parser.ts
new file mode 100644
index 0000000..0361ae5
--- /dev/null
+++ b/src/lib/utils/parser.ts
@@ -0,0 +1,143 @@
+import type { Image, User, Url } from '@prisma/client';
+
+export type ParseValue = {
+  file?: Image;
+  url?: Url;
+  user?: User;
+
+  link?: string;
+  raw_link?: string;
+};
+
+export function parseString(str: string, value: ParseValue) {
+  str = str.replace(/\{link\}/gi, value.link).replace(/\{raw_link\}/gi, value.raw_link);
+
+  const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?\}/gi;
+  let matches: RegExpMatchArray;
+
+  while ((matches = re.exec(str))) {
+    const getV = value[matches.groups.type];
+    if (!getV) {
+      str = replaceCharsFromString(str, '{unknown_type}', matches.index, re.lastIndex);
+      re.lastIndex = matches.index;
+      continue;
+    }
+
+    if (matches.groups.prop in ['password', 'avatar']) {
+      str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
+      re.lastIndex = matches.index;
+      continue;
+    }
+
+    const v = getV[matches.groups.prop];
+
+    if (v === undefined) {
+      str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
+      re.lastIndex = matches.index;
+      continue;
+    }
+
+    if (matches.groups.mod) {
+      str = replaceCharsFromString(str, modifier(matches.groups.mod, v), matches.index, re.lastIndex);
+      re.lastIndex = matches.index;
+      continue;
+    }
+
+    str = replaceCharsFromString(str, v, matches.index, re.lastIndex);
+    re.lastIndex = matches.index;
+  }
+
+  return str;
+}
+
+function modifier(mod: string, value: any): string {
+  mod = mod.toLowerCase();
+
+  if (value instanceof Date) {
+    switch (mod) {
+      case 'locale':
+        return value.toLocaleString();
+      case 'time':
+        return value.toLocaleTimeString();
+      case 'date':
+        return value.toLocaleDateString();
+      case 'unix':
+        return Math.floor(value.getTime() / 1000).toString();
+      case 'iso':
+        return value.toISOString();
+      case 'utc':
+        return value.toUTCString();
+      case 'year':
+        return value.getFullYear().toString();
+      case 'month':
+        return (value.getMonth() + 1).toString();
+      case 'day':
+        return value.getDate().toString();
+      case 'hour':
+        return value.getHours().toString();
+      case 'minute':
+        return value.getMinutes().toString();
+      case 'second':
+        return value.getSeconds().toString();
+      default:
+        return '{unknown_date_modifier}';
+    }
+  } else if (typeof value === 'string') {
+    switch (mod) {
+      case 'upper':
+        return value.toUpperCase();
+      case 'lower':
+        return value.toLowerCase();
+      case 'title':
+        return value.charAt(0).toUpperCase() + value.slice(1);
+      case 'length':
+        return value.length.toString();
+      case 'reverse':
+        return value.split('').reverse().join('');
+      case 'base64':
+        return btoa(value);
+      case 'hex':
+        return toHex(value);
+      default:
+        return '{unknown_str_modifier}';
+    }
+  } else if (typeof value === 'number') {
+    switch (mod) {
+      case 'comma':
+        return value.toLocaleString();
+      case 'hex':
+        return value.toString(16);
+      case 'octal':
+        return value.toString(8);
+      case 'binary':
+        return value.toString(2);
+      default:
+        return '{unknown_int_modifier}';
+    }
+  } else if (typeof value === 'boolean') {
+    switch (mod) {
+      case 'yesno':
+        return value ? 'Yes' : 'No';
+      case 'onoff':
+        return value ? 'On' : 'Off';
+      case 'truefalse':
+        return value ? 'True' : 'False';
+      default:
+        return '{unknown_bool_modifier}';
+    }
+  }
+
+  return '{unknown_modifier}';
+}
+
+function replaceCharsFromString(str: string, replace: string, start: number, end: number): string {
+  return str.slice(0, start) + replace + str.slice(end);
+}
+
+function toHex(str: string): string {
+  let hex = '';
+  for (let i = 0; i < str.length; i++) {
+    hex += '' + str.charCodeAt(i).toString(16);
+  }
+  return hex;
+}
diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts
index d17228d..552ba54 100644
--- a/src/pages/api/upload.ts
+++ b/src/pages/api/upload.ts
@@ -191,7 +191,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
           file,
           `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}/r/${
             invis ? invis.invis : file.file
-          }`
+          }`,
+          `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${
+            zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
+          }/${invis ? invis.invis : file.file}`
         );
       }
 
@@ -325,7 +328,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
         image,
         `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}/r/${
           invis ? invis.invis : image.file
-        }`
+        }`,
+        `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${
+          zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
+        }/${invis ? invis.invis : image.file}`
       );
     }
 
diff --git a/src/pages/view/[id].tsx b/src/pages/view/[id].tsx
index b035d37..b3e99bf 100644
--- a/src/pages/view/[id].tsx
+++ b/src/pages/view/[id].tsx
@@ -1,7 +1,7 @@
 import { Box, Button, Modal, PasswordInput } from '@mantine/core';
 import exts from 'lib/exts';
 import prisma from 'lib/prisma';
-import { parse } from 'lib/utils/client';
+import { parseString } from 'lib/utils/parser';
 import { GetServerSideProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
@@ -63,9 +63,14 @@ export default function EmbeddedFile({ image, user, pass, prismRender }) {
         {image.embed && (
           <>
             {user.embedSiteName && (
-              <meta property='og:site_name' content={parse(user.embedSiteName, image, user)} />
+              <meta
+                property='og:site_name'
+                content={parseString(user.embedSiteName, { file: image, user })}
+              />
+            )}
+            {user.embedTitle && (
+              <meta property='og:title' content={parseString(user.embedTitle, { file: image, user })} />
             )}
-            {user.embedTitle && <meta property='og:title' content={parse(user.embedTitle, image, user)} />}
             <meta property='theme-color' content={user.embedColor} />
           </>
         )}
diff --git a/src/server/index.ts b/src/server/index.ts
index 6074dba..bc3a95f 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -21,6 +21,7 @@ import rawRoute from './routes/raw';
 import uploadsRoute, { uploadsRouteOnResponse } from './routes/uploads';
 import urlsRoute, { urlsRouteOnResponse } from './routes/urls';
 import { IncomingMessage } from 'http';
+import { parseString } from '../lib/utils/parser';
 
 const dev = process.env.NODE_ENV === 'development';
 const logger = Logger.get('server');
@@ -38,6 +39,19 @@ start();
 async function start() {
   logger.debug('Starting server');
 
+  // const a = parseString(
+  //   '{file.name::upper} {file.mimetype} {file.test} {file.created_at::unix} {file.v::yesno} {file.int::hex}',
+  //   // @ts-ignore
+  //   {
+  //     name: 'test',
+  //     mimetype: 'image/png',
+  //     created_at: new Date(),
+  //     int: 123123123123123,
+  //     v: false,
+  //   }
+  // );
+  // console.log(a);
+
   // plugins
   server
     .register(loggerPlugin)