Release v2.1 (#441)

* feat: added datadog

* fix(deps): update dependency yup-locales-ko to v1.2.0

* fix: prevent perm missing

* fix: invalid start script

* fix(deps): update dependency formik to v2.2.9 (#409)

* chore: changed some header

* deps: updated sentry

* feat: added datadog metrix

* fix: error causing at custom git url

* chore: removed key file

CHANGED KEY

* types: holding missing flag

* feat: cors header

* feat: updated api docs

* deps: updated deps for security

* ci: handling sentry release

* ci: handling sentry

* Bug Fixes (#438)

* fix: invalid sql

* fix: fixed formatting number for null

close: #433

* chore: added more margin for ad

* typo: fixed typo issue

* Improved Report and changed email address (#440)

* feat: added report page for bot

* feat: added report page for user

* feat: blocking user reporting self

* feat: changed emails

* refactor: changed category handler style

* release: version changed to v2.1

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
Junseo Park 2021-07-04 12:56:19 +09:00 committed by GitHub
parent b17744fa3b
commit 282fc0836b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1962 additions and 1847 deletions

View File

@ -39,11 +39,6 @@ jobs:
with: with:
mysql database: 'discordbots' mysql database: 'discordbots'
mysql root password: 'test' mysql root password: 'test'
- name: Wait for MySQL
run: |
while ! mysqladmin ping --host=127.0.0.1 --password=test --silent; do
sleep 1
done
- name: Run Jest - name: Run Jest
run: yarn test run: yarn test
- name: Generate RSA Key Pair - name: Generate RSA Key Pair
@ -56,12 +51,14 @@ jobs:
run: | run: |
mv .env.demo.local .env.production.local mv .env.demo.local .env.production.local
printf 'MARIADB_ROOT_PASSWORD=YOUSHALLNOTPASS\nCOMMIT_HASH=${{ github.sha }}' > .env printf 'MARIADB_ROOT_PASSWORD=YOUSHALLNOTPASS\nCOMMIT_HASH=${{ github.sha }}' > .env
- name: Create needed files printf 'defaults.url=https://sentry.io/\ndefaults.org=koreanbots\ndefaults.project=client' > sentry.properties
run: echo '{"tester":"DEMO_KEY"}' > secret.json
- name: Build - name: Build
run: yarn build run: yarn build
env: env:
CI: true CI: true
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
# docker: # docker:
# needs: # needs:

View File

@ -6,4 +6,4 @@
## English ## English
Please [mail](mailto:koreanbots.dev@gmail.com) us! Please [mail](mailto:team@koreanbots.dev) us!

@ -1 +1 @@
Subproject commit 3636d4a1519f412ede7ab0b301dd8ca4bdd16a7b Subproject commit fcc3fb57a2bce58703acb6a2e9be4cfe98929427

View File

@ -0,0 +1,78 @@
import { FC, useState } from 'react'
import dynamic from 'next/dynamic'
import { FormikErrors, FormikTouched } from 'formik'
const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea'))
export const Check: FC<{ checked: boolean, text: string }> = ({ checked, text }) => <>
{checked && <i className='text-green-400 fas fa-check-circle mr-1' />}
{text}
</>
export const SubmitButton: FC = () => <div className='text-right'>
<Button type='submit'></Button>
</div>
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => <>
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' value={values.description} setValue={(value) => setFieldValue('description', value)} />
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
<SubmitButton />
</>
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
const [ isOwner, setOwner ] = useState(null)
const [ contacted, setContacted ] = useState(null)
return <div>
<h3 className='font-bold my-2'> ?</h3>
<Button onClick={() => setOwner(true)}>
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
</Button>
<Button onClick={() => setOwner(false)}>
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
</Button>
{
isOwner === true ? <>
<h3 className='font-bold my-2'> ?</h3>
<Button onClick={() => setContacted(true)}>
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
</Button>
<Button onClick={() => setContacted(false)}>
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
</Button>
{
contacted ? <div>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<ul className='text-gray-400 text-sm mb-1 list-disc list-inside'>
<li> ( )</li>
<li> ( , )</li>
</ul>
<p className='text-gray-400 text-sm mb-1'> <a className='text-blue-400' target='_blank' rel='noreferrer' href={`mailto:dmca@koreanbots.dev?subject=${encodeURI('[DMCA] 추가 컨텐츠')}&body=${encodeURI('디스코드 태그:')}`}>dmca@koreanbots.dev</a> , .</p>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
</div>
: contacted === false ? <>
<h2 className='font-bold mt-4 text-xl'> .</h2>
<p> , .</p>
</> : ''
}
</>
: isOwner === false ? <>
<h2 className='font-bold mt-4 text-xl'>, .</h2>
<p> , !</p>
</> : ''
}
</div>
}
interface ReportValues {
category: string | null
description: string
_csrf: string
}
interface ReportTemplateProps {
values?: ReportValues
errors?: FormikErrors<ReportValues>
touched?: FormikTouched<ReportValues>
setFieldValue?(field: string, value: unknown): void
}

View File

@ -2,7 +2,7 @@
use discordbots; use discordbots;
-- bots TABLE -- bots TABLE
ALTER TABLE `bots` CHANGE `servers` `servers` INT(11) NULL DEFAULT NULL, CHANGE `web` `web` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `git` `git` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `url` `url` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `category` `category` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '\'[]\'', CHANGE `status` `status` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `avatar` `avatar` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `tag` `tag` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, CHANGE `discord` `discord` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `state` `state` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '\'ok\'', CHANGE `vanity` `vanity` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `bg` `bg` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `banner` `banner` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL; ALTER TABLE `bots` CHANGE `servers` `servers` INT(11) NULL DEFAULT NULL, CHANGE `web` `web` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `git` `git` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `url` `url` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `category` `category` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '\'[]\'', CHANGE `status` `status` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `avatar` `avatar` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `tag` `tag` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, CHANGE `discord` `discord` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `state` `state` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ok', CHANGE `vanity` `vanity` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `bg` `bg` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, CHANGE `banner` `banner` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL;
-- USING NULL -- USING NULL
UPDATE `bots` SET web=NULL where web='false' or web=''; UPDATE `bots` SET web=NULL where web='false' or web='';

View File

@ -1,12 +1,12 @@
{ {
"name": "koreanbots", "name": "koreanbots",
"version": "2.0.0", "version": "2.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"pre-build": "git init && git submodule init && git submodule update --remote", "pre-build": "git init && git submodule init && git submodule update --remote",
"build": "npm run pre-build && next build", "build": "npm run pre-build && next build",
"start": "NODE_OPTIONS='--require dd-trace/init' next start | (sleep 1; wget http://localhost:3000/api/v2/management/load -O /dev/null)", "start": "NODE_OPTIONS='--require dd-trace/init' next start | (sleep 2; wget http://localhost:3000/api/v2/management/load -O /dev/null)",
"lint": "eslint --ext ts,tsx .", "lint": "eslint --ext ts,tsx .",
"prettier": "prettier --write **/*", "prettier": "prettier --write **/*",
"lint:fix": "eslint --ext ts,tsx . --fix", "lint:fix": "eslint --ext ts,tsx . --fix",
@ -16,10 +16,10 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "5.15.3", "@fortawesome/fontawesome-free": "5.15.3",
"@hcaptcha/react-hcaptcha": "0.3.6", "@hcaptcha/react-hcaptcha": "0.3.6",
"@sentry/nextjs": "6.5.1", "@sentry/nextjs": "6.8.0",
"@sentry/node": "6.5.1", "@sentry/node": "6.8.0",
"@sentry/react": "6.5.1", "@sentry/react": "6.8.0",
"@sentry/tracing": "6.5.1", "@sentry/tracing": "6.8.0",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"autoprefixer": "10.2.5", "autoprefixer": "10.2.5",
"badgen": "3.2.2", "badgen": "3.2.2",
@ -33,7 +33,7 @@
"emoji-mart": "3.0.1", "emoji-mart": "3.0.1",
"erlpack": "0.1.3", "erlpack": "0.1.3",
"express-rate-limit": "5.2.6", "express-rate-limit": "5.2.6",
"formik": "2.2.8", "formik": "2.2.9",
"generate-license-file": "1.1.0", "generate-license-file": "1.1.0",
"josa": "3.0.1", "josa": "3.0.1",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
@ -46,7 +46,7 @@
"next-session": "3.4.0", "next-session": "3.4.0",
"node-emoji": "1.10.0", "node-emoji": "1.10.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"postcss": "8.3.0", "postcss": "8.3.5",
"postcss-preset-env": "6.7.0", "postcss-preset-env": "6.7.0",
"rc-tooltip": "5.1.1", "rc-tooltip": "5.1.1",
"react": "17.0.2", "react": "17.0.2",
@ -59,7 +59,7 @@
"react-sortable-hoc": "2.0.0", "react-sortable-hoc": "2.0.0",
"react-use-clipboard": "1.0.7", "react-use-clipboard": "1.0.7",
"sanitize-html": "2.4.0", "sanitize-html": "2.4.0",
"tailwindcss": "2.1.4", "tailwindcss": "2.2.4",
"tlru": "1.0.2", "tlru": "1.0.2",
"twemoji": "13.1.0", "twemoji": "13.1.0",
"url-regex-safe": "2.0.2", "url-regex-safe": "2.0.2",

View File

@ -109,7 +109,7 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
<li> ? , .</li> <li> ? , .</li>
<li> ? , .</li> <li> ? , .</li>
( ) . <br/> ( ) . <br/>
<strong> (Slash Command) .</strong> . ( .) <strong> (Slash Command) .</strong> . ( .)
<ul> <ul>
<li>- 명령어: 도움, , , help, commands</li> <li>- 명령어: 도움, , , help, commands</li>
<li>- , <br/> <li>- , <br/>

View File

@ -1,6 +1,7 @@
import { NextApiRequest } from 'next' import { NextApiRequest } from 'next'
import rateLimit from 'express-rate-limit' import rateLimit from 'express-rate-limit'
import { MessageEmbed } from 'discord.js' import { MessageEmbed } from 'discord.js'
import tracer from 'dd-trace'
import { CaptchaVerify, get, put, remove, update } from '@utils/Query' import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
@ -83,6 +84,11 @@ const Bots = RequestHandler()
}) })
const userinfo = await get.user.load(user) const userinfo = await get.user.load(user)
await getBotReviewLogChannel().send(new MessageEmbed().setAuthor(`${userinfo.username}#${userinfo.tag}`, KoreanbotsEndPoints.URL.root + KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }), KoreanbotsEndPoints.URL.user(userinfo.id)).setTitle('대기 중').setColor('GREY').setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`).setTimestamp()) await getBotReviewLogChannel().send(new MessageEmbed().setAuthor(`${userinfo.username}#${userinfo.tag}`, KoreanbotsEndPoints.URL.root + KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }), KoreanbotsEndPoints.URL.user(userinfo.id)).setTitle('대기 중').setColor('GREY').setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`).setTimestamp())
await tracer.trace('botSubmits.submitted', (async span => {
span.setTag('id', result.id)
span.setTag('date', result.date)
span.setTag('user', userinfo.id)
}))
return ResponseWrapper(res, { code: 200, data: result }) return ResponseWrapper(res, { code: 200, data: result })
}) })
.delete(async (req: DeleteApiRequest, res) => { .delete(async (req: DeleteApiRequest, res) => {

View File

@ -1,5 +1,6 @@
import { NextApiRequest } from 'next' import { NextApiRequest } from 'next'
import { MessageEmbed } from 'discord.js' import { MessageEmbed } from 'discord.js'
import tracer from 'dd-trace'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
@ -19,8 +20,13 @@ const ApproveBotSubmit = RequestHandler()
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date })) get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
const embed = new MessageEmbed().setTitle('승인').setColor('GREEN').setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp() const embed = new MessageEmbed().setTitle('승인').setColor('GREEN').setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
if(req.body.note) embed.addField('📃 정보', req.body.note) if(req.body.reviewer) embed.addField('📃 정보', `심사자: ${req.body.reviewer}`)
await getBotReviewLogChannel().send(embed) await getBotReviewLogChannel().send(embed)
await tracer.trace('botSubmits.approve', (async span => {
span.setTag('id', submit.id)
span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer)
}))
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
}) })
@ -30,7 +36,7 @@ interface ApiRequest extends NextApiRequest {
date: string date: string
} }
body: { body: {
note?: string reviewer?: string
} }
} }

View File

@ -17,7 +17,7 @@ const DenyBotSubmit = RequestHandler()
await update.denyBotSubmission(submit.id, submit.date, req.body.reason) await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date })) get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
const embed = new MessageEmbed().setTitle('거부').setColor('RED').setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp() const embed = new MessageEmbed().setTitle('거부').setColor('RED').setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
if(req.body.note || req.body.reason) embed.addField('📃 정보', `${req.body.reason ? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: ''}${req.body.note ? `${req.body.note}` : ''}`) if(req.body.reviewer || req.body.reason) embed.addField('📃 정보', `${req.body.reason ? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: ''}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`)
await getBotReviewLogChannel().send(embed) await getBotReviewLogChannel().send(embed)
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
}) })
@ -29,7 +29,7 @@ interface ApiRequest extends NextApiRequest {
} }
body: { body: {
reason?: string reason?: string
note?: string reviewer: string
} }
} }

View File

@ -16,7 +16,7 @@ import { get } from '@utils/Query'
import Day from '@utils/Day' import Day from '@utils/Day'
import { ReportSchema } from '@utils/Yup' import { ReportSchema } from '@utils/Yup'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
import { checkBotFlag, checkUserFlag, formatNumber, parseCookie, redirectTo } from '@utils/Tools' import { checkBotFlag, checkUserFlag, formatNumber, parseCookie } from '@utils/Tools'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import NotFound from '../../404' import NotFound from '../../404'
@ -217,15 +217,11 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
/> />
))} ))}
<div className='list grid'> <div className='list grid'>
<a className='text-red-600 hover:underline cursor-pointer' onClick={() => { <Link href={`/bots/${router.query.id}/report`}>
if(!user) { <a className='text-red-600 hover:underline cursor-pointer' aria-hidden='true'>
localStorage.redirectTo = window.location.href <i className='far fa-flag' />
redirectTo(router, 'login') </a>
} </Link>
else setReportModal(true)
}} aria-hidden='true'>
<i className='far fa-flag' />
</a>
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => { <Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
setReportModal(false) setReportModal(false)
setReportRes(null) setReportRes(null)

140
pages/bots/[id]/report.tsx Normal file
View File

@ -0,0 +1,140 @@
import { NextPage } from 'next'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { Field, Form, Formik } from 'formik'
import { Bot, CsrfContext, ResponseProps, User } from '@types'
import { get } from '@utils/Query'
import { makeBotURL, parseCookie } from '@utils/Tools'
import { ParsedUrlQuery } from 'querystring'
import NotFound from 'pages/404'
import { getToken } from '@utils/Csrf'
import { DMCA, TextField } from '@components/ReportTemplate'
import { useState } from 'react'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
import { getJosaPicker } from 'josa'
import { reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message'))
const Login = dynamic(() => import('@components/Login'))
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
if(!data?.id) return <NotFound />
if(!user) return <Login>
<NextSeo title='신고하기' />
</Login>
return <Container paddingTop className='py-10'>
<NextSeo title={`${data.name} 신고하기`} />
<Link href={makeBotURL(data)}>
<a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} </a>
</Link>
{
reportRes?.code === 200 ? <Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p> . <strong> <a className='text-blue-600 hover:text-blue-500' href='/discord'> </a> !!</strong></p>
</Message> : <Formik onSubmit={async (body) => {
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
setReportRes(res)
}} validationSchema={ReportSchema} initialValues={{
category: null,
description: '',
_csrf: csrfToken
}}>
{
({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{
reportRes && <div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
}
<h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
{
reportCats.map(el =>
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
)
}
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category : null}</div>
{
values.category && <>
{
{
[reportCats[2]]: <Message type='info'>
<h3 className='font-bold text-xl'> ?</h3>
<p> .</p>
<p className='list-disc list-item list-inside'> 1393 | 1388</p>
</Message>,
[reportCats[5]]: <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />,
[reportCats[6]]: <Message type='warning'>
<h3 className='font-bold text-xl'> ?</h3>
<p><a className='text-blue-400' target='_blank' rel='noreferrer' href='http://dis.gd/report'> </a> .</p>
</Message>
}[values.category]
}
{
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
</>
}
</>
}
</div>
</Form>
)
}
</Formik>
}
</Container>
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.bot.load(ctx.query.id)
const user = await get.Authorization(parsed?.token)
return {
props: {
csrfToken: getToken(ctx.req, ctx.res),
data,
user: await get.user.load(user || '')
},
}
}
interface ReportBotProps {
csrfToken: string
data: Bot
user: User
}
interface Context extends CsrfContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default ReportBot

View File

@ -62,7 +62,7 @@ const VoteBot: NextPage<VoteBotProps> = ({ data, user, theme, csrfToken }) => {
<Link href={makeBotURL(data)}> <Link href={makeBotURL(data)}>
<a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} </a> <a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} </a>
</Link> </Link>
<Segment className='mb-10 py-8'> <Segment className='mb-16 py-8'>
<div className='text-center'> <div className='text-center'>
<DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4' /> <DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4' />
<Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark /> <Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark />

View File

@ -154,8 +154,8 @@ const PendingBot: NextPage<PendingBotProps> = ({ data }) => {
className='hover:underline' className='hover:underline'
href={data.git} href={data.git}
> >
<i className={`fab fa-${git?.[new URL(data.git).hostname].icon ?? 'git-alt'}`} /> <i className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`} />
{git?.[new URL(data.git).hostname].text ?? 'Git'} {git[new URL(data.git).hostname]?.text ?? 'Git'}
</a> </a>
)} )}
</div> </div>

View File

@ -9,6 +9,8 @@ const Docs = dynamic(() => import('@components/Docs'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar')) const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
const BODY = '중요도:\n설명:\n\n영향을 줄 수 있는 경우:'
const Security: NextPage<SecurityProps> = ({ bugReports }) => { const Security: NextPage<SecurityProps> = ({ bugReports }) => {
return <Docs return <Docs
header='버그 바운티 프로그램' header='버그 바운티 프로그램'
@ -59,7 +61,7 @@ const Security: NextPage<SecurityProps> = ({ bugReports }) => {
</ul> </ul>
<div className='text-center py-36'> <div className='text-center py-36'>
<h1 className='text-3xl font-bold mb-6'> ?</h1> <h1 className='text-3xl font-bold mb-6'> ?</h1>
<Button href='mailto:koreanbots.dev@gmail.com'></Button> <Button href={`mailto:team@koreanbots.dev?subject=[Security] &body=${encodeURI(BODY)}`}></Button>
</div> </div>
</Docs> </Docs>
} }

View File

@ -1,219 +0,0 @@
import { NextPage, NextPageContext } from 'next'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { SnowflakeUtil } from 'discord.js'
import { ParsedUrlQuery } from 'querystring'
import { josa } from 'josa'
import { Field, Form, Formik } from 'formik'
import { Bot, User, ResponseProps, Theme } from '@types'
import { get } from '@utils/Query'
import { checkUserFlag, parseCookie, redirectTo } from '@utils/Tools'
import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
import NotFound from '../404'
import { KoreanbotsEndPoints, reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Divider = dynamic(() => import('@components/Divider'))
const BotCard = dynamic(() => import('@components/BotCard'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
const Tag = dynamic(() => import('@components/Tag'))
const Advertisement = dynamic(() => import('@components/Advertisement'))
const Tooltip = dynamic(() => import('@components/Tooltip'))
const Message = dynamic(() => import('@components/Message'))
const Modal = dynamic(() => import('@components/Modal'))
const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea'))
const Users: NextPage<UserProps> = ({ user, data, csrfToken, theme }) => {
const router = useRouter()
const [ reportModal, setReportModal ] = useState(false)
const [ reportRes, setReportRes ] = useState<ResponseProps<null>>(null)
if (!data?.id) return <NotFound />
return (
<Container paddingTop className='py-10'>
<NextSeo
title={data.username}
description={data.bots.length === 0 ? `${data.username}님의 프로필입니다.` : josa(
`${(data.bots as Bot[])
.slice(0, 5)
.map(el => el.name)
.join(', ')}#{} .`
)}
openGraph={{
images: [{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'User Avatar'
}]
}}
/>
<div className='lg:flex'>
<div className='w-3/5 mx-auto text-center lg:w-1/6'>
<DiscordAvatar
size={512}
userID={data.id}
className='w-full'
/>
</div>
<div className='flex-grow px-5 py-10 w-full text-center lg:w-5/12 lg:text-left'>
<div>
<div className='lg:flex mt-3 mb-1 '>
<h1 className='text-4xl font-bold'>{data.username}</h1>
<span className='ml-0.5 text-gray-400 text-3xl font-semibold mt-1'>#{data.tag}</span>
</div>
<div className='badges flex mb-2 justify-center lg:justify-start'>
{checkUserFlag(data.flags, 'staff') && (
<Tooltip text='한국 디스코드봇 리스트 스탭입니다.' direction='left'>
<div className='pr-5 text-koreanbots-blue text-2xl'>
<i className='fas fa-hammer' />
</div>
</Tooltip>
)}
{checkUserFlag(data.flags, 'bughunter') && (
<Tooltip text='버그를 많이 제보해주신 분입니다.' direction='left'>
<div className='pr-5 text-green-500 text-2xl'>
<i className='fas fa-bug' />
</div>
</Tooltip>
)}
</div>
{data.github && (
<Tag
newTab
text={
<>
<i className='fab fa-github' /> {data.github}
</>
}
github
href={`https://github.com/${data.github}`}
/>
)}
<div className='list-none mt-2'>
<a className='text-red-600 hover:underline cursor-pointer' onClick={() => {
if(!user) {
localStorage.redirectTo = window.location.href
redirectTo(router, 'login')
}
else setReportModal(true)
}} aria-hidden='true'>
<i className='far fa-flag' />
</a>
</div>
<Modal header={user.id === data.id ? '자기 자신은 신고할 수 없습니다.' : `${data.username}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
setReportModal(false)
setReportRes(null)
}} full dark={theme === 'dark'}>
{
user.id === data.id ? <div className='text-center py-20'>
<h2 className='text-xl font-semibold'>
" 현명한 조언을 해주는 것은 자기 이외에는 없다. "
</h2>
</div> :
reportRes?.code === 200 ? <Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p> ! <a className='text-blue-600 hover:text-blue-500' href='/discord'> </a> </p>
</Message> : <Formik onSubmit={async (body) => {
const res = await Fetch<null>(`/users/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
setReportRes(res)
}} validationSchema={ReportSchema} initialValues={{
category: null,
description: '',
_csrf: csrfToken
}}>
{
({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{
reportRes && <div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
}
<h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
{
reportCats.map(el =>
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
)
}
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category : null}</div>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.description} setValue={(value) => setFieldValue('description', value)} />
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
</div>
<div className='text-right'>
<Button className='bg-gray-500 hover:opacity-90 text-white' onClick={()=> setReportModal(false)}></Button>
<Button type='submit' className='bg-red-500 hover:opacity-90 text-white'></Button>
</div>
</Form>
)
}
</Formik>
}
</Modal>
</div>
</div>
</div>
<Divider />
<h2 className='mt-8 text-3xl font-bold'> </h2>
{data.bots.length === 0 ? <h2 className='text-xl'> .</h2> :
<ResponsiveGrid>
{
(data.bots as Bot[]).map((bot: Bot) => (
<BotCard key={bot.id} bot={bot} />
))
}
</ResponsiveGrid>
}
<Advertisement />
</Container>
)
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) || ''
const data = await get.user.load(ctx.query.id)
return { props: { user: await get.user.load(user) || {}, data, date: SnowflakeUtil.deconstruct(data?.id ?? '0')?.date?.toJSON(), csrfToken: getToken(ctx.req, ctx.res) } }
}
interface UserProps {
user: User
data: User
csrfToken: string
theme: Theme
}
interface Context extends NextPageContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default Users

145
pages/users/[id]/index.tsx Normal file
View File

@ -0,0 +1,145 @@
import { NextPage, NextPageContext } from 'next'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { SnowflakeUtil } from 'discord.js'
import { ParsedUrlQuery } from 'querystring'
import { josa } from 'josa'
import { Bot, User, Theme } from '@types'
import { get } from '@utils/Query'
import { checkUserFlag, parseCookie } from '@utils/Tools'
import { getToken } from '@utils/Csrf'
import NotFound from '../../404'
import { KoreanbotsEndPoints } from '@utils/Constants'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Divider = dynamic(() => import('@components/Divider'))
const BotCard = dynamic(() => import('@components/BotCard'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
const Tag = dynamic(() => import('@components/Tag'))
const Advertisement = dynamic(() => import('@components/Advertisement'))
const Tooltip = dynamic(() => import('@components/Tooltip'))
const Users: NextPage<UserProps> = ({ user, data }) => {
const router = useRouter()
if (!data?.id) return <NotFound />
return (
<Container paddingTop className='py-10'>
<NextSeo
title={data.username}
description={data.bots.length === 0 ? `${data.username}님의 프로필입니다.` : josa(
`${(data.bots as Bot[])
.slice(0, 5)
.map(el => el.name)
.join(', ')}#{} .`
)}
openGraph={{
images: [{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'User Avatar'
}]
}}
/>
<div className='lg:flex'>
<div className='w-3/5 mx-auto text-center lg:w-1/6'>
<DiscordAvatar
size={512}
userID={data.id}
className='w-full'
/>
</div>
<div className='flex-grow px-5 py-10 w-full text-center lg:w-5/12 lg:text-left'>
<div>
<div className='lg:flex mt-3 mb-1 '>
<h1 className='text-4xl font-bold'>{data.username}</h1>
<span className='ml-0.5 text-gray-400 text-3xl font-semibold mt-1'>#{data.tag}</span>
</div>
<div className='badges flex mb-2 justify-center lg:justify-start'>
{checkUserFlag(data.flags, 'staff') && (
<Tooltip text='한국 디스코드봇 리스트 스탭입니다.' direction='left'>
<div className='pr-5 text-koreanbots-blue text-2xl'>
<i className='fas fa-hammer' />
</div>
</Tooltip>
)}
{checkUserFlag(data.flags, 'bughunter') && (
<Tooltip text='버그를 많이 제보해주신 분입니다.' direction='left'>
<div className='pr-5 text-green-500 text-2xl'>
<i className='fas fa-bug' />
</div>
</Tooltip>
)}
</div>
{data.github && (
<Tag
newTab
text={
<>
<i className='fab fa-github' /> {data.github}
</>
}
github
href={`https://github.com/${data.github}`}
/>
)}
{
user?.id !== data.id && <div className='list-none mt-2'>
<Link href={`/users/${router.query.id}/report`}>
<a className='text-red-600 hover:underline cursor-pointer' aria-hidden='true'>
<i className='far fa-flag' />
</a>
</Link>
</div>
}
</div>
</div>
</div>
<Divider />
<h2 className='mt-8 text-3xl font-bold'> </h2>
{data.bots.length === 0 ? <h2 className='text-xl'> .</h2> :
<ResponsiveGrid>
{
(data.bots as Bot[]).map((bot: Bot) => (
<BotCard key={bot.id} bot={bot} />
))
}
</ResponsiveGrid>
}
<Advertisement />
</Container>
)
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) || ''
const data = await get.user.load(ctx.query.id)
return { props: { user: await get.user.load(user) || {}, data, date: SnowflakeUtil.deconstruct(data?.id ?? '0')?.date?.toJSON(), csrfToken: getToken(ctx.req, ctx.res) } }
}
interface UserProps {
user: User
data: User
csrfToken: string
theme: Theme
}
interface Context extends NextPageContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default Users

143
pages/users/[id]/report.tsx Normal file
View File

@ -0,0 +1,143 @@
import { NextPage } from 'next'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { Field, Form, Formik } from 'formik'
import { CsrfContext, ResponseProps, User } from '@types'
import { get } from '@utils/Query'
import { makeUserURL, parseCookie } from '@utils/Tools'
import { ParsedUrlQuery } from 'querystring'
import NotFound from 'pages/404'
import { getToken } from '@utils/Csrf'
import { DMCA, TextField } from '@components/ReportTemplate'
import { useState } from 'react'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
import { getJosaPicker } from 'josa'
import { reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message'))
const Login = dynamic(() => import('@components/Login'))
const ReportUser: NextPage<ReportUserProps> = ({ data, user, csrfToken }) => {
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
if(!data?.id) return <NotFound />
if(!user) return <Login>
<NextSeo title='신고하기' />
</Login>
if(user?.id === data.id) return <>
<div className='flex items-center justify-center h-screen select-none'>
<div className='container mx-auto px-20 md:text-left text-center'>
<h1 className='text-6xl font-semibold'>..!</h1>
<p className='text-gray-400 text-lg mt-2'> .</p>
</div>
</div>
</>
return <Container paddingTop className='py-10'>
<NextSeo title={`${data.username} 신고하기`} />
<Link href={makeUserURL(data)}>
<a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.username}</strong>{getJosaPicker('로')(data.username)} </a>
</Link>
{
reportRes?.code === 200 ? <Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p> . <strong> <a className='text-blue-600 hover:text-blue-500' href='/discord'> </a> !!</strong></p>
</Message> : <Formik onSubmit={async (body) => {
const res = await Fetch(`/users/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
setReportRes(res)
}} validationSchema={ReportSchema} initialValues={{
category: null,
description: '',
_csrf: csrfToken
}}>
{
({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{
reportRes && <div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
}
<h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
{
reportCats.map(el =>
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
)
}
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category : null}</div>
{
values.category && <>
{
values.category === '오픈소스 라이선스, 저작권 위반 등 권리 침해' ? <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} /> :
values.category === '괴롭힘, 모욕, 명예훼손' ? <>
<Message type='info'>
<h3 className='font-bold text-xl'> ?</h3>
<p> .</p>
<p className='list-disc list-item list-inside'> 1393 | 1388</p>
</Message>
</> : ''
}
{
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
</>
}
</>
}
</div>
</Form>
)
}
</Formik>
}
</Container>
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.user.load(ctx.query.id)
const user = await get.Authorization(parsed?.token)
return {
props: {
csrfToken: getToken(ctx.req, ctx.res),
data,
user: await get.user.load(user || '')
},
}
}
interface ReportUserProps {
csrfToken: string
data: User
user: User
}
interface Context extends CsrfContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default ReportUser

View File

@ -1,5 +0,0 @@
defaults.url=https://sentry.io/
defaults.org=koreanbots
defaults.project=client
auth.token=85b0389aa5db4388b8f09b1f1884ea5f964e65cdac83408ab7be303eec2b3305
cli.executable=../../.npm/_npx/32022/lib/node_modules/@sentry/wizard/node_modules/@sentry/cli/bin/sentry-cli

View File

@ -60,6 +60,7 @@ export enum UserFlags {
export enum BotFlags { export enum BotFlags {
general = 0 << 0, general = 0 << 0,
official = 1 << 0, official = 1 << 0,
holding = 1 << 1,
trusted = 1 << 2, trusted = 1 << 2,
partnered = 1 << 3, partnered = 1 << 3,
verified = 1 << 4, verified = 1 << 4,
@ -203,13 +204,13 @@ export type Category =
| '검색' | '검색'
export type ReportCategory = export type ReportCategory =
| '불법' | '위법'
| '봇을 이용한 테러'
| '괴롭힘, 모욕, 명예훼손' | '괴롭힘, 모욕, 명예훼손'
| '스팸, 도배, 의미없는 텍스트' | '스팸, 도배, 의미없는 텍스트'
| '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠' | '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠'
| '라이선스혹은 권리 침해' | '오픈소스 라이선스, 저작권 위반 등 권리 침해'
| 'Discord ToS 위반' | 'Discord ToS를 위반하거나 한국 디스코드봇 리스트 가이드라인 위반'
| 'Koreanbots 가이드라인 위반'
| '기타' | '기타'
export type ListType = 'VOTE' | 'TRUSTED' | 'NEW' | 'PARTNERED' | 'CATEGORY' | 'SEARCH' export type ListType = 'VOTE' | 'TRUSTED' | 'NEW' | 'PARTNERED' | 'CATEGORY' | 'SEARCH'

View File

@ -120,13 +120,13 @@ export const categoryIcon = {
} }
export const reportCats = [ export const reportCats = [
'불법', '위법',
'봇을 이용한 테러',
'괴롭힘, 모욕, 명예훼손', '괴롭힘, 모욕, 명예훼손',
'스팸, 도배, 의미없는 텍스트', '스팸, 도배, 의미없는 텍스트',
'폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠', '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠',
'라이선스혹은 권리 침해', '오픈소스 라이선스, 저작권 위반 등 권리 침해',
'Discord ToS 위반', 'Discord ToS를 위반하거나 한국 디스코드봇 리스트 가이드라인 위반',
'Koreanbots 가이드라인 위반',
'기타', '기타',
] ]

View File

@ -22,4 +22,4 @@ interface ApiResponse {
status(status: string|number): void status(status: string|number): void
setHeader(key: string, value: string): void setHeader(key: string, value: string): void
json(json: unknown): void json(json: unknown): void
} }

View File

@ -24,6 +24,7 @@ export function handlePWA(): boolean {
} }
export function formatNumber(value: number):string { export function formatNumber(value: number):string {
if(!value) return '0'
const suffixes = ['', '만', '억', '조','해'] const suffixes = ['', '만', '억', '조','해']
const suffixNum = Math.floor((''+value).length/4) const suffixNum = Math.floor((''+value).length/4)
let shortValue:string|number = parseFloat((suffixNum != 0 ? (value / Math.pow(10000,suffixNum)) : value).toPrecision(2)) let shortValue:string|number = parseFloat((suffixNum != 0 ? (value / Math.pow(10000,suffixNum)) : value).toPrecision(2))
@ -50,10 +51,13 @@ export function makeImageURL(root:string, { format='png', size=256 }:ImageOption
return `${root}.${format}?size=${size}` return `${root}.${format}?size=${size}`
} }
export function makeBotURL({id, vanity, flags=0}: { flags?: number, vanity?:string, id: string }): string { export function makeBotURL({ id, vanity, flags=0 }: { flags?: number, vanity?:string, id: string }): string {
return `/bots/${(checkBotFlag(flags, 'trusted') || checkBotFlag(flags, 'partnered')) && vanity ? vanity : id}` return `/bots/${(checkBotFlag(flags, 'trusted') || checkBotFlag(flags, 'partnered')) && vanity ? vanity : id}`
} }
export function makeUserURL({ id }: { id: string }): string {
return `/users/${id}`
}
export function serialize<T>(data: T): T { export function serialize<T>(data: T): T {
return JSON.parse(JSON.stringify(data)) return JSON.parse(JSON.stringify(data))
} }

2971
yarn.lock

File diff suppressed because it is too large Load Diff