refactor/webhook (#553)

* types: fix type of shortValue

* refactor: move webhook section to dev page

* feat: add tooltip

* refactor: remove warning prop for Label

* refactor: remove webhook field from edit page

* feat: check webhookStatus

* chore: remove unused import

* chore: remove ununsed import

* chore: remove unused import

* chore: add desc about failed webhook

* chore: remove unused import

* feat: remove warning icon when updated

* feat: add webhook field to server

* refactor: remove webhook verification

* fix: endpoint

* feat: add endpoint for /applications/servers/[id]

* feat: remove webhook related props from bot/server

* chore: edit url

* feat: add link to docs

* chore: remove indents
This commit is contained in:
SKINMAKER 2023-04-14 23:57:43 +09:00 committed by GitHub
parent 336300bd9a
commit 59dbf74466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 209 additions and 144 deletions

View File

@ -1,5 +1,3 @@
import Tooltip from '@components/Tooltip'
const Label: React.FC<LabelProps> = ({ const Label: React.FC<LabelProps> = ({
For, For,
children, children,
@ -8,9 +6,7 @@ const Label: React.FC<LabelProps> = ({
error = null, error = null,
grid = true, grid = true,
short = false, short = false,
required = false, required = false
warning = false,
warningText
}) => { }) => {
return <label return <label
className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'} className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'}
@ -23,11 +19,6 @@ const Label: React.FC<LabelProps> = ({
{required && ( {required && (
<span className='align-text-top text-red-500 text-base font-semibold'> *</span> <span className='align-text-top text-red-500 text-base font-semibold'> *</span>
)} )}
{warning && (
<Tooltip direction='left' text={warningText}>
<span className='text-red-500 text-base font-semibold pl-1' role='img' aria-label='warning'></span>
</Tooltip>
)}
</h3> </h3>
{labelDesc} {labelDesc}
</div> </div>

View File

@ -6,7 +6,9 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { checkToken } from '@utils/Csrf' import { checkToken } from '@utils/Csrf'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
import { User } from '@types' import { User, WebhookStatus } from '@types'
import { parseWebhookURL } from 'discord.js'
import { verifyWebhook } from '@utils/Webhook'
const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => { const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
@ -24,7 +26,25 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 }) if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
await update.updateBotApplication(req.query.id, { webhook: validated.webhook || null }) if(validated.webhookURL) {
const key = await verifyWebhook(validated.webhookURL)
if(key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
}
await update.webhook(req.query.id, 'bots', {
url: validated.webhookURL,
status: parseWebhookURL(validated.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
failedSince: null,
secret: key,
})
} else {
await update.webhook(req.query.id, 'bots', {
url: null,
status: WebhookStatus.None,
failedSince: null,
secret: null,
})
}
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
}) })

View File

@ -0,0 +1,58 @@
import { NextApiRequest } from 'next'
import { DeveloperServer, DeveloperServerSchema } from '@utils/Yup'
import { get, update } from '@utils/Query'
import ResponseWrapper from '@utils/ResponseWrapper'
import { checkToken } from '@utils/Csrf'
import RequestHandler from '@utils/RequestHandler'
import { WebhookStatus } from '@types'
import { parseWebhookURL } from 'discord.js'
import { verifyWebhook } from '@utils/Webhook'
const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return
const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const server = await get.serverData(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
if (![server.owner, ...server.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
if(validated.webhookURL) {
const key = await verifyWebhook(validated.webhookURL)
if(key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
}
await update.webhook(req.query.id, 'servers', {
url: validated.webhookURL,
status: parseWebhookURL(validated.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
failedSince: null,
secret: key,
})
} else {
await update.webhook(req.query.id, 'servers', {
url: null,
status: WebhookStatus.None,
failedSince: null,
secret: null,
})
}
return ResponseWrapper(res, { code: 200 })
})
interface ApiRequest extends NextApiRequest {
body: DeveloperServer
query: {
id: string
}
}
export default ServerApplications

View File

@ -12,7 +12,6 @@ import { User } from '@types'
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools' import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
import { discordLog, getBotReviewLogChannel, getMainGuild } from '@utils/DiscordBot' import { discordLog, getBotReviewLogChannel, getMainGuild } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
import { verifyWebhook } from '@utils/Webhook'
const patchLimiter = rateLimit({ const patchLimiter = rateLimit({
windowMs: 2 * 60 * 1000, windowMs: 2 * 60 * 1000,
@ -29,8 +28,6 @@ const Bots = RequestHandler()
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
else { else {
delete bot.webhookURL
delete bot.webhookStatus
return ResponseWrapper(res, { code: 200, data: bot }) return ResponseWrapper(res, { code: 200, data: bot })
} }
}) })
@ -158,18 +155,15 @@ const Bots = RequestHandler()
}) })
if (!validated) return if (!validated) return
const key = validated.webhookURL ? await verifyWebhook(validated.webhookURL) : null
if(key === false) { const result = await update.bot(req.query.id, validated)
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
}
const result = await update.bot(req.query.id, validated, key)
if(result === 0) return ResponseWrapper(res, { code: 400 }) if(result === 0) return ResponseWrapper(res, { code: 400 })
else { else {
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`) const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)
const diffData = objectDiff( const diffData = objectDiff(
{ prefix: bot.prefix, library: bot.lib, web: bot.web, git: bot.git, url: bot.url, discord: bot.discord, webhook: bot.webhookURL, intro: bot.intro, category: JSON.stringify(bot.category) }, { prefix: bot.prefix, library: bot.lib, web: bot.web, git: bot.git, url: bot.url, discord: bot.discord, intro: bot.intro, category: JSON.stringify(bot.category) },
{ prefix: validated.prefix, library: validated.library, web: validated.website, git: validated.git, url: validated.url, discord: validated.discord, webhook: validated.webhookURL, intro: validated.intro, category: JSON.stringify(validated.category) } { prefix: validated.prefix, library: validated.library, web: validated.website, git: validated.git, url: validated.url, discord: validated.discord, intro: validated.intro, category: JSON.stringify(validated.category) }
) )
diffData.forEach(d => { diffData.forEach(d => {
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff') embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')

View File

@ -10,7 +10,6 @@ import RequestHandler from '@utils/RequestHandler'
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools' import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
import { DiscordBot, discordLog } from '@utils/DiscordBot' import { DiscordBot, discordLog } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
import { verifyWebhook } from '@utils/Webhook'
const patchLimiter = rateLimit({ const patchLimiter = rateLimit({
windowMs: 2 * 60 * 1000, windowMs: 2 * 60 * 1000,
@ -27,8 +26,6 @@ const Servers = RequestHandler()
const server = await get.server.load(req.query.id) const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' }) if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
else { else {
delete server.webhookURL
delete server.webhookStatus
return ResponseWrapper(res, { code: 200, data: server }) return ResponseWrapper(res, { code: 200, data: server })
} }
}) })
@ -138,12 +135,7 @@ const Servers = RequestHandler()
const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null) const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null)
if(invite?.guild.id !== server.id || invite.expiresAt) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 초대코드입니다.', errors: ['입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.', '만료되지 않는 초대코드인지 확인해주세요.'] }) if(invite?.guild.id !== server.id || invite.expiresAt) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 초대코드입니다.', errors: ['입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.', '만료되지 않는 초대코드인지 확인해주세요.'] })
const key = validated.webhook ? await verifyWebhook(validated.webhook) : null const result = await update.server(req.query.id, validated)
if(key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
}
const result = await update.server(req.query.id, validated, key)
if(result === 0) return ResponseWrapper(res, { code: 400 }) if(result === 0) return ResponseWrapper(res, { code: 400 })
else { else {

View File

@ -12,7 +12,7 @@ import { get } from '@utils/Query'
import { checkUserFlag, cleanObject, makeBotURL, parseCookie, redirectTo } from '@utils/Tools' import { checkUserFlag, cleanObject, makeBotURL, parseCookie, redirectTo } from '@utils/Tools'
import { ManageBot, ManageBotSchema } from '@utils/Yup' import { ManageBot, ManageBotSchema } from '@utils/Yup'
import { botCategories, library } from '@utils/Constants' import { botCategories, library } from '@utils/Constants'
import { Bot, Theme, User, WebhookStatus } from '@types' import { Bot, Theme, User } from '@types'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
@ -75,7 +75,6 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
url: bot.url, url: bot.url,
git: bot.git, git: bot.git,
discord: bot.discord, discord: bot.discord,
webhookURL: bot.webhookURL,
_csrf: csrfToken _csrf: csrfToken
})} })}
validationSchema={ManageBotSchema} validationSchema={ManageBotSchema}
@ -146,9 +145,6 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
discord.gg/<Input name='discord' placeholder='JEh53MQ' /> discord.gg/<Input name='discord' placeholder='JEh53MQ' />
</div> </div>
</Label> </Label>
<Label For='webhookURL' label='웹훅 링크' labelDesc='봇의 업데이트 알림을 받을 웹훅 링크를 입력해주세요. (웹훅 링크가 유효하지 않을 경우 웹훅이 중지되며, 다시 저장할 경우 작동합니다.)' error={errors.webhookURL && touched.webhookURL ? errors.webhookURL : null} warning={bot.webhookStatus === WebhookStatus.Disabled} warningText='웹훅 링크가 유효하지 않아 웹훅이 중지되었습니다.'>
<Input name='webhookURL' placeholder='https://discord.com/api/webhooks/ID/TOKEN'/>
</Label>
<Divider /> <Divider />
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required> <Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
<Input name='intro' placeholder='국내 봇을 한 곳에서.' /> <Input name='intro' placeholder='국내 봇을 한 곳에서.' />

View File

@ -1,3 +1,4 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import { NextPage, NextPageContext } from 'next' import { NextPage, NextPageContext } from 'next'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -5,15 +6,19 @@ import { useState } from 'react'
import useCopyClipboard from 'react-use-clipboard' import useCopyClipboard from 'react-use-clipboard'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { parseCookie, redirectTo } from '@utils/Tools' import { cleanObject, parseCookie, redirectTo } from '@utils/Tools'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
import { ParsedUrlQuery } from 'querystring' import { ParsedUrlQuery } from 'querystring'
import { Bot, BotSpec, ResponseProps, Theme } from '@types' import { Bot, BotSpec, ResponseProps, Theme, WebhookStatus } from '@types'
import NotFound from 'pages/404' import NotFound from 'pages/404'
import Link from 'next/link' import Link from 'next/link'
import { Form, Formik } from 'formik'
import { DeveloperBot, DeveloperBotSchema } from '@utils/Yup'
import Input from '@components/Form/Input'
import Tooltip from '@components/Tooltip'
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
const DeveloperLayout = dynamic(() => import('@components/DeveloperLayout')) const DeveloperLayout = dynamic(() => import('@components/DeveloperLayout'))
@ -29,13 +34,13 @@ const BotApplication: NextPage<BotApplicationProps> = ({ user, spec, bot, theme,
const [ tokenCopied, setTokenCopied ] = useCopyClipboard(spec?.token, { const [ tokenCopied, setTokenCopied ] = useCopyClipboard(spec?.token, {
successDuration: 1000 successDuration: 1000
}) })
// async function updateApplication(d: DeveloperBot) { async function updateApplication(d: DeveloperBot) {
// const res = await Fetch(`/applications/bots/${bot.id}`, { const res = await Fetch(`/applications/bots/${bot.id}`, {
// method: 'PATCH', method: 'PATCH',
// body: JSON.stringify(cleanObject(d)) body: JSON.stringify(cleanObject(d))
// }) })
// setData(res) setData(res)
// } }
async function resetToken() { async function resetToken() {
const res = await Fetch<{ token: string }>(`/applications/bots/${bot.id}/reset`, { const res = await Fetch<{ token: string }>(`/applications/bots/${bot.id}/reset`, {
@ -102,23 +107,33 @@ const BotApplication: NextPage<BotApplicationProps> = ({ user, spec, bot, theme,
</div> </div>
</Modal> </Modal>
</div> </div>
{/* <Formik validationSchema={DeveloperBotSchema} initialValues={{ <Formik validationSchema={DeveloperBotSchema} initialValues={{
webhook: spec.webhook || '', webhookURL: spec.webhookURL || '',
_csrf: csrfToken _csrf: csrfToken
}} }}
onSubmit={(data) => updateApplication(data)}> onSubmit={updateApplication}>
{({ errors, touched }) => ( {({ errors, touched }) => (
<Form> <Form>
<div className='mb-2'> <div className='mb-2'>
<h3 className='font-bold mb-1'> URL</h3> <h3 className='font-bold mb-1'>
<p className='text-gray-400 text-sm mb-1'> .</p> URL
<Input name='webhook' placeholder='https://webhook.kbots.link' /> {(!data || data.code !== 200) && spec.webhookStatus === WebhookStatus.Disabled && (
{touched.webhook && errors.webhook ? <div className='text-red-500 text-xs font-light mt-1'>{errors.webhook}</div> : null} <Tooltip direction='left' text='웹훅 링크가 유효하지 않아 웹훅이 중지되었습니다.'>
<span className='text-red-500 text-base font-semibold pl-1' role='img' aria-label='warning'></span>
</Tooltip>
)}
</h3>
<p className='text-gray-400 text-sm mb-1'> .<br/>
, .<br/>
<Link href={'/developers/docs/%EC%9B%B9%ED%9B%84%ED%81%AC'}><a className='text-blue-500 hover:text-blue-400 font-semibold'> </a></Link> .
</p>
<Input name='webhookURL' placeholder='https://webhook.koreanbots.dev' />
{touched.webhookURL && errors.webhookURL ? <div className='text-red-500 text-xs font-light mt-1'>{errors.webhookURL}</div> : null}
</div> </div>
<Button type='submit'><i className='far fa-save'/> </Button> <Button type='submit'><i className='far fa-save'/> </Button>
</Form> </Form>
)} )}
</Formik> */} </Formik>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,15 +5,19 @@ import { useState } from 'react'
import useCopyClipboard from 'react-use-clipboard' import useCopyClipboard from 'react-use-clipboard'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { parseCookie, redirectTo } from '@utils/Tools' import { cleanObject, parseCookie, redirectTo } from '@utils/Tools'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
import { ParsedUrlQuery } from 'querystring' import { ParsedUrlQuery } from 'querystring'
import { Server, BotSpec, ResponseProps, Theme } from '@types' import { Server, BotSpec, ResponseProps, Theme, WebhookStatus } from '@types'
import NotFound from 'pages/404' import NotFound from 'pages/404'
import Link from 'next/link' import Link from 'next/link'
import { DeveloperServer, DeveloperServerSchema } from '@utils/Yup'
import { Form, Formik } from 'formik'
import Tooltip from '@components/Tooltip'
import Input from '@components/Form/Input'
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
const DeveloperLayout = dynamic(() => import('@components/DeveloperLayout')) const DeveloperLayout = dynamic(() => import('@components/DeveloperLayout'))
@ -29,13 +33,13 @@ const ServerApplication: NextPage<ServerApplicationProps> = ({ user, spec, serve
const [ tokenCopied, setTokenCopied ] = useCopyClipboard(spec?.token, { const [ tokenCopied, setTokenCopied ] = useCopyClipboard(spec?.token, {
successDuration: 1000 successDuration: 1000
}) })
// async function updateApplication(d: DeveloperBot) { async function updateApplication(d: DeveloperServer) {
// const res = await Fetch(`/applications/bots/${bot.id}`, { const res = await Fetch(`/applications/servers/${server.id}`, {
// method: 'PATCH', method: 'PATCH',
// body: JSON.stringify(cleanObject(d)) body: JSON.stringify(cleanObject(d))
// }) })
// setData(res) setData(res)
// } }
async function resetToken() { async function resetToken() {
const res = await Fetch<{ token: string }>(`/applications/servers/${server.id}/reset`, { const res = await Fetch<{ token: string }>(`/applications/servers/${server.id}/reset`, {
@ -104,23 +108,33 @@ const ServerApplication: NextPage<ServerApplicationProps> = ({ user, spec, serve
</div> </div>
</Modal> </Modal>
</div> </div>
{/* <Formik validationSchema={DeveloperBotSchema} initialValues={{ <Formik validationSchema={DeveloperServerSchema} initialValues={{
webhook: spec.webhook || '', webhookURL: spec.webhookURL || '',
_csrf: csrfToken _csrf: csrfToken
}} }}
onSubmit={(data) => updateApplication(data)}> onSubmit={updateApplication}>
{({ errors, touched }) => ( {({ errors, touched }) => (
<Form> <Form>
<div className='mb-2'> <div className='mb-2'>
<h3 className='font-bold mb-1'> URL</h3> <h3 className='font-bold mb-1'>
<p className='text-gray-400 text-sm mb-1'> .</p> URL
<Input name='webhook' placeholder='https://webhook.kbots.link' /> {(!data || data.code !== 200) && spec.webhookStatus === WebhookStatus.Disabled && (
{touched.webhook && errors.webhook ? <div className='text-red-500 text-xs font-light mt-1'>{errors.webhook}</div> : null} <Tooltip direction='left' text='웹훅 링크가 유효하지 않아 웹훅이 중지되었습니다.'>
<span className='text-red-500 text-base font-semibold pl-1' role='img' aria-label='warning'></span>
</Tooltip>
)}
</h3>
<p className='text-gray-400 text-sm mb-1'> .<br/>
, .<br/>
<Link href={'/developers/docs/%EC%9B%B9%ED%9B%84%ED%81%AC'}><a className='text-blue-500 hover:text-blue-400 font-semibold'> </a></Link> .
</p>
<Input name='webhookURL' placeholder='https://webhook.koreanbots.dev' />
{touched.webhookURL && errors.webhookURL ? <div className='text-red-500 text-xs font-light mt-1'>{errors.webhookURL}</div> : null}
</div> </div>
<Button type='submit'><i className='far fa-save'/> </Button> <Button type='submit'><i className='far fa-save'/> </Button>
</Form> </Form>
)} )}
</Formik> */} </Formik>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@ import { get } from '@utils/Query'
import { checkUserFlag, cleanObject, getRandom, makeServerURL, parseCookie, redirectTo } from '@utils/Tools' import { checkUserFlag, cleanObject, getRandom, makeServerURL, parseCookie, redirectTo } from '@utils/Tools'
import { ManageServer, ManageServerSchema } from '@utils/Yup' import { ManageServer, ManageServerSchema } from '@utils/Yup'
import { serverCategories, ServerIntroList } from '@utils/Constants' import { serverCategories, ServerIntroList } from '@utils/Constants'
import { Server, Theme, User, WebhookStatus } from '@types' import { Server, Theme, User } from '@types'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
@ -57,7 +57,6 @@ const ManageServerPage:NextPage<ManageServerProps> = ({ server, user, owners, cs
intro: server.intro, intro: server.intro,
desc: server.desc, desc: server.desc,
category: server.category, category: server.category,
webhookURL: server.webhookURL,
_csrf: csrfToken _csrf: csrfToken
})} })}
validationSchema={ManageServerSchema} validationSchema={ManageServerSchema}
@ -99,9 +98,6 @@ const ManageServerPage:NextPage<ManageServerProps> = ({ server, user, owners, cs
discord.gg/<Input name='invite' placeholder='JEh53MQ' /> discord.gg/<Input name='invite' placeholder='JEh53MQ' />
</div> </div>
</Label> </Label>
<Label For='webhookURL' label='웹훅 링크' labelDesc='봇의 업데이트 알림을 받을 웹훅 링크를 입력해주세요. (웹훅 링크가 유효하지 않을 경우 웹훅이 중지되며, 다시 저장할 경우 작동합니다.)' error={errors.webhookURL && touched.webhookURL ? errors.webhookURL : null} warning={server.webhookStatus === WebhookStatus.Disabled} warningText='웹훅 링크가 유효하지 않아 웹훅이 중지되었습니다.'>
<Input name='webhookURL' placeholder='https://discord.com/api/webhooks/ID/TOKEN'/>
</Label>
<Divider /> <Divider />
<Label For='intro' label='서버 소개' labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required> <Label For='intro' label='서버 소개' labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
<Input name='intro' placeholder={getRandom(ServerIntroList)} /> <Input name='intro' placeholder={getRandom(ServerIntroList)} />

View File

@ -24,8 +24,6 @@ export interface Bot {
git: string | null git: string | null
url: string | null url: string | null
discord: string | null discord: string | null
webhookURL: string | null
webhookStatus: WebhookStatus
vanity: string | null vanity: string | null
bg: string bg: string
banner: string banner: string
@ -55,8 +53,6 @@ export interface Server {
desc: string desc: string
category: ServerCategory[] category: ServerCategory[]
invite: string invite: string
webhookURL: string | null
webhookStatus: WebhookStatus
vanity: string | null vanity: string | null
bg: string | null bg: string | null
banner: string | null banner: string | null
@ -89,7 +85,15 @@ export interface User {
export interface BotSpec { export interface BotSpec {
id: string id: string
webhook: string | null webhookURL: string | null
webhookStatus: WebhookStatus
token: string
}
export interface ServerSpec {
id: string
webhookURL: string | null
webhookStatus: WebhookStatus
token: string token: string
} }

View File

@ -1,9 +1,9 @@
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { TLRU } from 'tlru' import { TLRU } from 'tlru'
import DataLoader from 'dataloader' import DataLoader from 'dataloader'
import { ActivityType, GuildFeature, GuildMember, parseWebhookURL, User as DiscordUser, UserFlags } from 'discord.js' import { ActivityType, GuildFeature, GuildMember, User as DiscordUser, UserFlags } from 'discord.js'
import { Bot, Server, User, ListType, List, TokenRegister, BotFlags, DiscordUserFlags, SubmittedBot, DiscordTokenInfo, ServerData, ServerFlags, RawGuild, Nullable, WebhookStatus, Webhook } from '@types' import { Bot, Server, User, ListType, List, TokenRegister, BotFlags, DiscordUserFlags, SubmittedBot, DiscordTokenInfo, ServerData, ServerFlags, RawGuild, Nullable, Webhook, BotSpec, ServerSpec } from '@types'
import { botCategories, DiscordEnpoints, imageSafeHost, serverCategories, SpecialEndPoints, VOTE_COOLDOWN } from './Constants' import { botCategories, DiscordEnpoints, imageSafeHost, serverCategories, SpecialEndPoints, VOTE_COOLDOWN } from './Constants'
import knex from './Knex' import knex from './Knex'
@ -37,8 +37,6 @@ async function getBot(id: string, topLevel=true):Promise<Bot> {
'trusted', 'trusted',
'partnered', 'partnered',
'discord', 'discord',
'webhook_url',
'webhook_status',
'state', 'state',
'vanity', 'vanity',
'bg', 'bg',
@ -68,12 +66,8 @@ async function getBot(id: string, topLevel=true):Promise<Bot> {
} else { } else {
res[0].status = null res[0].status = null
} }
res[0].webhookURL = res[0].webhook_url
res[0].webhookStatus = res[0].webhook_status
delete res[0].trusted delete res[0].trusted
delete res[0].partnered delete res[0].partnered
delete res[0].webhook_url
delete res[0].webhook_status
if (topLevel) { if (topLevel) {
res[0].owners = await Promise.all( res[0].owners = await Promise.all(
res[0].owners.map(async (u: string) => await get._rawUser.load(u)) res[0].owners.map(async (u: string) => await get._rawUser.load(u))
@ -101,8 +95,6 @@ async function getServer(id: string, topLevel=true): Promise<Server> {
'category', 'category',
'invite', 'invite',
'state', 'state',
'webhook_url',
'webhook_status',
'vanity', 'vanity',
'bg', 'bg',
'banner', 'banner',
@ -120,10 +112,6 @@ async function getServer(id: string, topLevel=true): Promise<Server> {
await knex('servers').update({ name: data.name, owners: JSON.stringify([data.owner, ...data.admins]) }) await knex('servers').update({ name: data.name, owners: JSON.stringify([data.owner, ...data.admins]) })
.where({ id: res[0].id }) .where({ id: res[0].id })
} }
res[0].webhookURL = res[0].webhook_url
res[0].webhookStatus = res[0].webhook_status
delete res[0].webhook_url
delete res[0].webhook_status
delete res[0].owners delete res[0].owners
res[0].icon = data?.icon || null res[0].icon = data?.icon || null
res[0].members = data?.memberCount || null res[0].members = data?.memberCount || null
@ -487,16 +475,26 @@ async function submitServer(userID: string, id: string, data: AddServerSubmit):
return true return true
} }
async function getBotSpec(id: string, userID: string) { async function getBotSpec(id: string, userID: string): Promise<BotSpec | null> {
const res = await knex('bots').select(['id', 'token']).where({ id }).andWhere('owners', 'like', `%${userID}%`) const res = await knex('bots').select(['id', 'token', 'webhook_url', 'webhook_status']).where({ id }).andWhere('owners', 'like', `%${userID}%`)
if(!res[0]) return null if(!res[0]) return null
return serialize(res[0]) return {
id: res[0].id,
token: res[0].token,
webhookURL: res[0].webhook_url,
webhookStatus: res[0].webhook_status
}
} }
async function getServerSpec(id: string, userID: string): Promise<{ id: string, token: string }> { async function getServerSpec(id: string, userID: string): Promise<ServerSpec | null> {
const res = await knex('servers').select(['id', 'token']).where({ id }).andWhere('owners', 'like', `%${userID}%`) const res = await knex('servers').select(['id', 'token', 'webhook_url', 'webhook_status']).where({ id }).andWhere('owners', 'like', `%${userID}%`)
if(!res[0]) return null if(!res[0]) return null
return serialize(res[0]) return {
id: res[0].id,
token: res[0].token,
webhookURL: res[0].webhook_url,
webhookStatus: res[0].webhook_status
}
} }
async function deleteBot(id: string): Promise<boolean> { async function deleteBot(id: string): Promise<boolean> {
@ -510,7 +508,7 @@ async function deleteServer(id: string): Promise<boolean> {
return !!server return !!server
} }
async function updateBot(id: string, data: ManageBot, webhookSecret: string | null): Promise<number> { async function updateBot(id: string, data: ManageBot): Promise<number> {
const res = await knex('bots').where({ id }) const res = await knex('bots').where({ id })
if(res.length === 0) return 0 if(res.length === 0) return 0
await knex('bots').update({ await knex('bots').update({
@ -520,10 +518,6 @@ async function updateBot(id: string, data: ManageBot, webhookSecret: string | nu
git: data.git, git: data.git,
url: data.url, url: data.url,
discord: data.discord, discord: data.discord,
webhook_url: data.webhookURL,
webhook_status: parseWebhookURL(data.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
webhook_failed_since: null,
webhook_secret: webhookSecret,
category: JSON.stringify(data.category), category: JSON.stringify(data.category),
intro: data.intro, intro: data.intro,
desc: data.desc desc: data.desc
@ -532,18 +526,14 @@ async function updateBot(id: string, data: ManageBot, webhookSecret: string | nu
return 1 return 1
} }
async function updatedServer(id: string, data: ManageServer, webhookSecret: string | null) { async function updatedServer(id: string, data: ManageServer) {
const res = await knex('servers').where({ id }) const res = await knex('servers').where({ id })
if(res.length === 0) return 0 if(res.length === 0) return 0
await knex('servers').update({ await knex('servers').update({
invite: data.invite, invite: data.invite,
category: JSON.stringify(data.category), category: JSON.stringify(data.category),
intro: data.intro, intro: data.intro,
desc: data.desc, desc: data.desc
webhook_url: data.webhookURL,
webhook_status: parseWebhookURL(data.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
webhook_failed_since: null,
webhook_secret: webhookSecret,
}).where({ id }) }).where({ id })
return 1 return 1
@ -578,12 +568,6 @@ async function updateWebhook(id: string, type: 'bots' | 'servers', value: Partia
return true return true
} }
async function updateBotApplication(id: string, value: { webhook: string }) {
const bot = await knex('bots').update({ webhook: value.webhook }).where({ id })
if(bot !== 1) return false
return true
}
async function updateOwner(id: string, owners: string[]): Promise<void> { async function updateOwner(id: string, owners: string[]): Promise<void> {
await knex('bots').where({ id }).update({ owners: JSON.stringify(owners) }) await knex('bots').where({ id }).update({ owners: JSON.stringify(owners) })
get.bot.clear(id) get.bot.clear(id)
@ -892,7 +876,6 @@ export const get = {
export const update = { export const update = {
assignToken, assignToken,
updateBotApplication,
resetBotToken, resetBotToken,
resetServerToken, resetServerToken,
updateServer, updateServer,

View File

@ -28,11 +28,11 @@ export function formatNumber(value: number):string {
if(!value) return '0' 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: number = parseFloat((suffixNum != 0 ? (value / Math.pow(10000, suffixNum)) : value).toPrecision(2))
if (shortValue % 1 != 0) { if (shortValue % 1 != 0) {
shortValue = shortValue.toFixed(1) shortValue = Number(shortValue.toFixed(1))
} }
if(suffixNum === 1 && shortValue < 1) return Number(shortValue) * 10 + '천' if(suffixNum === 1 && shortValue < 1) return shortValue * 10 + '천'
else if(shortValue === 1000) return '1천' else if(shortValue === 1000) return '1천'
return shortValue+suffixes[suffixNum] return shortValue+suffixes[suffixNum]
} }

View File

@ -326,11 +326,6 @@ export const ManageBotSchema: Yup.SchemaOf<ManageBot> = Yup.object({
.min(2, '지원 디스코드는 최소 2자여야합니다.') .min(2, '지원 디스코드는 최소 2자여야합니다.')
.max(32, '지원 디스코드는 최대 32자까지만 가능합니다.') .max(32, '지원 디스코드는 최대 32자까지만 가능합니다.')
.nullable(), .nullable(),
webhookURL: Yup.string()
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
.matches(Url, '올바른 웹훅 URL을 입력해주세요.')
.max(256, '웹훅 링크는 최대 256자까지만 가능합니다.')
.nullable(),
category: Yup.array(Yup.string().oneOf(botCategories)) category: Yup.array(Yup.string().oneOf(botCategories))
.min(1, '최소 한 개의 카테고리를 선택해주세요.') .min(1, '최소 한 개의 카테고리를 선택해주세요.')
.unique('카테고리는 중복될 수 없습니다.') .unique('카테고리는 중복될 수 없습니다.')
@ -353,7 +348,6 @@ export interface ManageBot {
url: string url: string
git: string git: string
discord: string discord: string
webhookURL: string
category: string[] category: string[]
intro: string intro: string
desc: string desc: string
@ -378,11 +372,6 @@ export const ManageServerSchema: Yup.SchemaOf<ManageServer> = Yup.object({
.min(100, '서버 설명은 최소 100자여야합니다.') .min(100, '서버 설명은 최소 100자여야합니다.')
.max(1500, '서버 설명은 최대 1500자여야합니다.') .max(1500, '서버 설명은 최대 1500자여야합니다.')
.required('서버 설명은 필수 항목입니다.'), .required('서버 설명은 필수 항목입니다.'),
webhookURL: Yup.string()
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
.matches(Url, '올바른 웹훅 URL을 입력해주세요.')
.max(256, '웹훅 링크는 최대 256자까지만 가능합니다.')
.nullable(),
_csrf: Yup.string().required(), _csrf: Yup.string().required(),
}) })
@ -391,7 +380,6 @@ export interface ManageServer {
category: string[] category: string[]
intro: string intro: string
desc: string desc: string
webhookURL: string
_csrf: string _csrf: string
} }
@ -406,7 +394,7 @@ export interface CsrfCaptcha {
} }
export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({ export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
webhook: Yup.string() webhookURL: Yup.string()
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.') .matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
.matches(Url, '올바른 웹훅 URL을 입력해주세요.') .matches(Url, '올바른 웹훅 URL을 입력해주세요.')
.max(150, 'URL은 최대 150자까지만 가능합니다.') .max(150, 'URL은 최대 150자까지만 가능합니다.')
@ -415,7 +403,21 @@ export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
}) })
export interface DeveloperBot { export interface DeveloperBot {
webhook: string | null webhookURL: string | null
_csrf: string
}
export const DeveloperServerSchema: Yup.SchemaOf<DeveloperServer> = Yup.object({
webhookURL: Yup.string()
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
.matches(Url, '올바른 웹훅 URL을 입력해주세요.')
.max(150, 'URL은 최대 150자까지만 가능합니다.')
.nullable(),
_csrf: Yup.string().required(),
})
export interface DeveloperServer {
webhookURL: string | null
_csrf: string _csrf: string
} }