diff --git a/pages/bots/[id]/index.tsx b/pages/bots/[id]/index.tsx index 620699c..63de652 100644 --- a/pages/bots/[id]/index.tsx +++ b/pages/bots/[id]/index.tsx @@ -36,7 +36,7 @@ const TextArea = dynamic(() => import('@components/Form/TextArea')) const Modal = dynamic(() => import('@components/Modal')) const NSFW = dynamic(() => import('@components/NSFW')) -const Bots: NextPage = ({ data, date, user, theme, csrfToken }) => { +const Bots: NextPage = ({ data, desc, date, user, theme, csrfToken }) => { const bg = checkBotFlag(data?.flags, 'trusted') && data?.banner const router = useRouter() const [ nsfw, setNSFW ] = useState() @@ -305,7 +305,7 @@ const Bots: NextPage = ({ data, date, user, theme, csrfToken }) => { }
- +
@@ -320,10 +320,12 @@ const Bots: NextPage = ({ data, date, user, theme, csrfToken }) => { export const getServerSideProps = async (ctx: Context) => { const parsed = parseCookie(ctx.req) const data = await get.bot.load(ctx.query.id) ?? { id: '' } + const desc = await get.botDescSafe(data.id) const user = await get.Authorization(parsed?.token) return { props: { data, + desc, date: SnowflakeUtil.deconstruct(data.id ?? '0').date.toJSON(), user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) @@ -335,6 +337,7 @@ export default Bots interface BotsProps { data: Bot + desc: string date: Date user: User theme: Theme diff --git a/utils/Constants.ts b/utils/Constants.ts index c2d238e..e49267a 100644 --- a/utils/Constants.ts +++ b/utils/Constants.ts @@ -122,6 +122,12 @@ export const reportCats = [ '기타', ] +export const imageSafeHost = [ + 'koreanbots.dev', + 'githubusercontent.com', + 'cdn.discordapp.com' +] + export const MessageColor = { success: 'bg-green-200 text-green-800', error: 'bg-red-200 text-red-800', @@ -131,7 +137,8 @@ export const MessageColor = { export const BASE_URLs = { api: 'https://discord.com/api', - cdn: 'https://cdn.discordapp.com' + cdn: 'https://cdn.discordapp.com', + camo: 'https://camo.koreanbots.dev' } export const BotBadgeType = (data: Bot) => { diff --git a/utils/Query.ts b/utils/Query.ts index 8577530..1cd2631 100644 --- a/utils/Query.ts +++ b/utils/Query.ts @@ -4,13 +4,14 @@ import DataLoader from 'dataloader' import { User as DiscordUser } from 'discord.js' import { Bot, User, ListType, BotList, TokenRegister, BotFlags, DiscordUserFlags, SubmittedBot } from '@types' -import { categories, SpecialEndPoints } from './Constants' +import { categories, imageSafeHost, SpecialEndPoints } from './Constants' import knex from './Knex' import { DiscordBot, getMainGuild } from './DiscordBot' import { sign, verify } from './Jwt' -import { formData, serialize } from './Tools' +import { camoUrl, formData, serialize } from './Tools' import { AddBotSubmit, ManageBot } from './Yup' +import { markdownImage } from './Regex' export const imageRateLimit = new TLRU({ maxAgeMs: 60000 }) @@ -43,7 +44,6 @@ async function getBot(id: string, owners=true):Promise { .orWhere({ vanity: id, trusted: true }) .orWhere({ vanity: id, partnered: true }) if (res[0]) { - const discordBot = await get.discord.user.load(res[0].id) await getMainGuild()?.members?.fetch(res[0].id).catch(e=> e) if(!discordBot) return null @@ -353,6 +353,17 @@ export const get = { async (ids: string[]) => (await Promise.all(ids.map(async (el: string) => await getBot(el, false)))).map(row => serialize(row)) , { cacheMap: new TLRU({ maxStoreSize: 50, maxAgeMs: 60000 }) }), + botDescSafe: async (id: string) => { + const bot = await get.bot.load(id) + return bot?.desc.replace(markdownImage, (matches: string, alt: string|undefined, link: string|undefined, description: string|undefined): string => { + try { + const url = new URL(link) + return `![${alt || description || ''}](${imageSafeHost.find(el => url.host.endsWith(el)) ? link : camoUrl(link) })` + } catch { + return matches + } + }) || null + }, user: new DataLoader( async (ids: string[]) => (await Promise.all(ids.map(async (el: string) => await getUser(el)))).map(row => serialize(row)) diff --git a/utils/Regex.ts b/utils/Regex.ts index b9cb648..acc42e5 100644 --- a/utils/Regex.ts +++ b/utils/Regex.ts @@ -11,3 +11,4 @@ export const Emoji = export const Heading = '(.*?)<\\/h(\\d)>' export const EmojiSyntax = ':(\\w+):' export const ImageTag = /]*?alt\s*=\s*['"]([^'"]*?)['"][^>]*?>/ +export const markdownImage = /!\[([^\]]*)\]\((.*?)\s*("(?:.*[^"])")?\s*\)/g \ No newline at end of file diff --git a/utils/ShowdownExtensions.ts b/utils/ShowdownExtensions.ts index 662080c..01d1f61 100644 --- a/utils/ShowdownExtensions.ts +++ b/utils/ShowdownExtensions.ts @@ -5,7 +5,7 @@ import { KoreanbotsEmoji } from './Constants' export const anchorHeader = { type: 'output', regex: Heading, - replace: function (__match: string, id:string, title:string, level:number) { + replace: function (__match: string, id:string, title:string, level:number): string { // github anchor style const href = id.replace(ImageTag, '$1').replace(/"/gi, '') @@ -33,7 +33,7 @@ export const twemoji = { export const customEmoji = { type: 'output', regex: EmojiSyntax, - replace: function(__match: string, name: string) { + replace: function(__match: string, name: string): string { const result = KoreanbotsEmoji.find(el => el.short_names.includes(name)) if(!name || !result) return `:${name}:` return `${name}` diff --git a/utils/Tools.ts b/utils/Tools.ts index 848fc27..319f4d7 100644 --- a/utils/Tools.ts +++ b/utils/Tools.ts @@ -1,8 +1,9 @@ +import { createHmac } from 'crypto' import { Readable } from 'stream' import cookie from 'cookie' import { BotFlags, ImageOptions, UserFlags } from '@types' -import { KoreanbotsEndPoints, Oauth } from './Constants' +import { BASE_URLs, KoreanbotsEndPoints, Oauth } from './Constants' import { NextRouter } from 'next/router' export function formatNumber(value: number):string { @@ -121,4 +122,21 @@ export function cleanObject>(obj: T): T { return obj } -export { anchorHeader, twemoji, customEmoji } from './ShowdownExtensions' \ No newline at end of file +export function camoUrl(url: string): string { + return BASE_URLs.camo + `/${HMAC(url)}/${toHex(url)}` +} + +export function HMAC(value: string, secret=process.env.CAMO_SECRET):string|null { + try { + return createHmac('sha1', secret).update(value, 'utf8').digest('hex') + } + catch { + return null + } +} + +export function toHex(value: string): string { + return Buffer.from(value).toString('hex') +} + +export * from './ShowdownExtensions' \ No newline at end of file