Compare commits

...

8 Commits

Author SHA1 Message Date
skinmaker1345
c24c979931 fix: typo 2025-06-27 22:22:31 +09:00
skinmaker1345
fd5f3c537e chore: add license related enforcements 2025-06-27 20:17:28 +09:00
skinmaker1345
f4d388cca7 fix: do not send vanity log when no changes are made 2025-06-27 20:11:08 +09:00
skinmaker1345
c9df8d01e3 chore: specify enforcement description 2025-06-27 20:11:08 +09:00
skinmaker1345
5cdb0f5dd1 feat: add alert page 2025-06-27 20:11:08 +09:00
skinmaker1345
7a983d7d96 feat: add enforcements field 2025-06-27 20:11:08 +09:00
skinmaker1345
9aaf66018f fix: remove authorization check to /servers/:id/owners 2025-06-22 00:12:56 +09:00
SKINMAKER
a2adbf116e
feat: make all API Requests require authorization (#686) 2025-06-21 22:18:53 +09:00
17 changed files with 209 additions and 49 deletions

View File

@ -21,7 +21,7 @@ const Label: React.FC<LabelProps> = ({
<span className='align-text-top text-base font-semibold text-red-500'> *</span> <span className='align-text-top text-base font-semibold text-red-500'> *</span>
)} )}
</h3> </h3>
{labelDesc} <span className='whitespace-pre-line'>{labelDesc}</span>
</div> </div>
)} )}
<div className={short ? 'col-span-1' : 'col-span-3'}> <div className={short ? 'col-span-1' : 'col-span-3'}>

View File

@ -109,7 +109,10 @@ const Select: React.FC<SelectProps> = ({
onChange={handleChange} onChange={handleChange}
onBlur={handleTouch} onBlur={handleTouch}
noOptionsMessage={() => '검색 결과가 없습니다.'} noOptionsMessage={() => '검색 결과가 없습니다.'}
value={values.map((el) => ({ label: el, value: el }))} value={values.map((el) => ({
label: Object.values(options).find(({ value }) => value === el)?.label || el,
value: el,
}))}
components={{ components={{
MultiValue, MultiValue,
MultiValueRemove, MultiValueRemove,

View File

@ -10,7 +10,7 @@ import HCaptcha from '@hcaptcha/react-hcaptcha'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { cleanObject, parseCookie, redirectTo } from '@utils/Tools' import { cleanObject, parseCookie, redirectTo } from '@utils/Tools'
import { AddBotSubmit, AddBotSubmitSchema } from '@utils/Yup' import { AddBotSubmit, AddBotSubmitSchema } from '@utils/Yup'
import { botCategories, botCategoryDescription, library } from '@utils/Constants' import { botCategories, botCategoryDescription, botEnforcements, library } from '@utils/Constants'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
import { ResponseProps, SubmittedBot, Theme, User } from '@types' import { ResponseProps, SubmittedBot, Theme, User } from '@types'
@ -57,6 +57,7 @@ const AddBot: NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
- -
- -
- ?`, - ?`,
enforcements: [],
_csrf: csrfToken, _csrf: csrfToken,
_captcha: 'captcha', _captcha: 'captcha',
} }
@ -356,6 +357,34 @@ const AddBot: NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
</Segment> </Segment>
</Label> </Label>
<Divider /> <Divider />
<Label
For='enforcements'
label='필수 고지 내용'
labelDesc='내용에 해당하는 경우 필수로 선택해야 합니다.'
required
error={
errors.enforcements && touched.enforcements ? (errors.enforcements as string) : null
}
>
<Selects
options={Object.entries(botEnforcements).map(([k, v]) => ({
label: k,
value: v,
}))}
handleChange={(value) => {
setFieldValue(
'enforcements',
value.map((v) => v.value)
)
}}
handleTouch={() => setFieldTouched('enforcements', true)}
values={values.enforcements ?? ([] as string[])}
setValues={(value) => {
setFieldValue('enforcements', value)
}}
/>
</Label>
<Divider />
<p className='mb-5 mt-2 text-base'> <p className='mb-5 mt-2 text-base'>
<span className='font-semibold text-red-500'> *</span> = <span className='font-semibold text-red-500'> *</span> =
</p> </p>

View File

@ -39,6 +39,10 @@ const patchLimiter = rateLimit({
}) })
const Bots = RequestHandler() const Bots = RequestHandler()
.get(async (req: GetApiRequest, res) => { .get(async (req: GetApiRequest, res) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
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 {
@ -219,7 +223,8 @@ const Bots = RequestHandler()
const csrfValidated = checkToken(req, res, req.body._csrf) const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return if (!csrfValidated) return
const validated: ManageBot = await getManageBotSchema(isPerkAvailable).validate(req.body, { abortEarly: false }) const validated: ManageBot = await getManageBotSchema(isPerkAvailable)
.validate(req.body, { abortEarly: false })
.then((el) => el) .then((el) => el)
.catch((e) => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
@ -241,28 +246,29 @@ const Bots = RequestHandler()
errors: ['다른 커스텀 URL로 다시 시도해주세요.'], errors: ['다른 커스텀 URL로 다시 시도해주세요.'],
}) })
} }
if (validated.vanity !== bot.vanity) {
await webhookClients.internal.noticeLog.send({ await webhookClients.internal.noticeLog.send({
embeds: [ embeds: [
{ {
title: '한디리 커스텀 URL 변경', title: '한디리 커스텀 URL 변경',
description: `봇: ${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot( description: `봇: ${bot.name} - <@${bot.id}> ([${
bot.id bot.id
)}))`, }](${KoreanbotsEndPoints.URL.bot(bot.id)}))`,
fields: [ fields: [
{ {
name: '이전', name: '이전',
value: bot.vanity || '없음', value: bot.vanity || '없음',
}, },
{ {
name: '이후', name: '이후',
value: validated.vanity || '없음', value: validated.vanity || '없음',
}, },
], ],
color: Colors.Blue, color: Colors.Blue,
}, },
], ],
}) })
}
} }
const result = await update.bot(req.query.id, validated) const result = await update.bot(req.query.id, validated)
if (result === 0) return ResponseWrapper(res, { code: 400 }) if (result === 0) return ResponseWrapper(res, { code: 400 })
@ -284,6 +290,7 @@ const Bots = RequestHandler()
category: JSON.stringify(bot.category), category: JSON.stringify(bot.category),
vanity: bot.vanity, vanity: bot.vanity,
banner: bot.banner, banner: bot.banner,
enforcements: JSON.stringify(bot.enforcements),
bg: bot.bg, bg: bot.bg,
}, },
{ {
@ -297,6 +304,7 @@ const Bots = RequestHandler()
category: JSON.stringify(validated.category), category: JSON.stringify(validated.category),
vanity: validated.vanity, vanity: validated.vanity,
banner: validated.banner, banner: validated.banner,
enforcements: JSON.stringify(validated.enforcements),
bg: validated.bg, bg: validated.bg,
} }
) )

View File

@ -4,7 +4,11 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { Bot, List } from '@types' import { Bot, List } from '@types'
const NewList = RequestHandler().get(async (_req, res) => { const NewList = RequestHandler().get(async (req, res) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const result = await get.list.new.load(1) const result = await get.list.new.load(1)
return ResponseWrapper<List<Bot>>(res, { code: 200, data: result }) return ResponseWrapper<List<Bot>>(res, { code: 200, data: result })
}) })

View File

@ -6,6 +6,10 @@ import { Bot, List } from '@types'
import Yup from '@utils/Yup' import Yup from '@utils/Yup'
const VotesList = RequestHandler().get(async (req, res) => { const VotesList = RequestHandler().get(async (req, res) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const page = await Yup.number() const page = await Yup.number()
.positive() .positive()
.integer() .integer()

View File

@ -8,6 +8,10 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Bot, Server, List } from '@types' import { Bot, Server, List } from '@types'
const Search = RequestHandler().get(async (req: ApiRequest, res) => { const Search = RequestHandler().get(async (req: ApiRequest, res) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 }) const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
.then((el) => el) .then((el) => el)
.catch((e) => { .catch((e) => {

View File

@ -8,6 +8,10 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Bot, List } from '@types' import { Bot, List } from '@types'
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => { const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const validated = await SearchQuerySchema.validate({ const validated = await SearchQuerySchema.validate({
q: req.query.q || req.query.query, q: req.query.q || req.query.query,
page: req.query.page, page: req.query.page,

View File

@ -8,6 +8,10 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Server, List } from '@types' import { Server, List } from '@types'
const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => { const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const validated = await SearchQuerySchema.validate({ const validated = await SearchQuerySchema.validate({
q: req.query.q || req.query.query, q: req.query.q || req.query.query,
page: req.query.page, page: req.query.page,

View File

@ -36,6 +36,10 @@ const patchLimiter = rateLimit({
}) })
const Servers = RequestHandler() const Servers = RequestHandler()
.get(async (req: GetApiRequest, res) => { .get(async (req: GetApiRequest, res) => {
const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
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 {

View File

@ -5,7 +5,10 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
const Users = RequestHandler().get(async (req: ApiRequest, res) => { const Users = RequestHandler().get(async (req: ApiRequest, res) => {
console.log(req.query) const auth = req.headers.authorization
? await get.BotAuthorization(req.headers.authorization)
: await get.Authorization(req.cookies.token)
if (!auth) return ResponseWrapper(res, { code: 401 })
const user = await get.user.load(req.query?.id) const user = await get.user.load(req.query?.id)
if (!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' }) if (!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' })
else return ResponseWrapper(res, { code: 200, data: user }) else return ResponseWrapper(res, { code: 200, data: user })

View File

@ -9,9 +9,16 @@ import { ParsedUrlQuery } from 'querystring'
import { getJosaPicker } from 'josa' import { getJosaPicker } from 'josa'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { checkBotFlag, checkUserFlag, cleanObject, makeBotURL, parseCookie, redirectTo } from '@utils/Tools' import {
checkBotFlag,
checkUserFlag,
cleanObject,
makeBotURL,
parseCookie,
redirectTo,
} from '@utils/Tools'
import { ManageBot, getManageBotSchema } from '@utils/Yup' import { ManageBot, getManageBotSchema } from '@utils/Yup'
import { botCategories, botCategoryDescription, library } from '@utils/Constants' import { botCategories, botCategoryDescription, botEnforcements, library } from '@utils/Constants'
import { Bot, Theme, User } 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'
@ -71,6 +78,7 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
) )
return <Forbidden /> return <Forbidden />
const isPerkAvailable = checkBotFlag(bot.flags, 'trusted') || checkBotFlag(bot.flags, 'partnered') const isPerkAvailable = checkBotFlag(bot.flags, 'trusted') || checkBotFlag(bot.flags, 'partnered')
console.log(bot.enforcements)
return ( return (
<Container paddingTop className='pb-10 pt-5'> <Container paddingTop className='pb-10 pt-5'>
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' /> <NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
@ -82,6 +90,7 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
prefix: bot.prefix, prefix: bot.prefix,
library: bot.lib, library: bot.lib,
category: bot.category, category: bot.category,
enforcements: bot.enforcements,
intro: bot.intro, intro: bot.intro,
desc: bot.desc, desc: bot.desc,
website: bot.web, website: bot.web,
@ -98,8 +107,12 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
> >
{({ errors, touched, values, setFieldTouched, setFieldValue }) => ( {({ errors, touched, values, setFieldTouched, setFieldValue }) => (
<Form> <Form>
<div className='text-center md:flex md:text-left'> <div className='text-ceznter md:flex md:text-left'>
<DiscordAvatar userID={bot.id} className='mx-auto rounded-full md:mx-1' hash={bot.avatar}/> <DiscordAvatar
userID={bot.id}
className='mx-auto rounded-full md:mx-1'
hash={bot.avatar}
/>
<div className='px-8 py-6 md:w-2/3'> <div className='px-8 py-6 md:w-2/3'>
<h1 className='text-3xl font-bold'> <h1 className='text-3xl font-bold'>
{bot.name}#{bot.tag} {bot.name}#{bot.tag}
@ -165,7 +178,11 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
error={errors.category && touched.category ? (errors.category as string) : null} error={errors.category && touched.category ? (errors.category as string) : null}
> >
<Selects <Selects
options={botCategories.map((el) => ({ label: el, value: el, description: botCategoryDescription[el] }))} options={botCategories.map((el) => ({
label: el,
value: el,
description: botCategoryDescription[el],
}))}
handleChange={(value) => { handleChange={(value) => {
setFieldValue( setFieldValue(
'category', 'category',
@ -273,24 +290,26 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
<Markdown text={values.desc} /> <Markdown text={values.desc} />
</Segment> </Segment>
</Label> </Label>
{ {isPerkAvailable && (
isPerkAvailable && ( <>
<>
<Divider /> <Divider />
<h2 className='pt-2 text-2xl font-semibold text-koreanbots-green'> </h2> <h2 className='pt-2 text-2xl font-semibold text-koreanbots-green'>
<span className='mt-1 text-sm text-gray-400'> . ( URL과 / .)</span>
</h2>
<span className='mt-1 text-sm text-gray-400'>
. ( URL과 /
.)
</span>
<Label <Label
For='vanity' For='vanity'
label='한디리 커스텀 URL' label='한디리 커스텀 URL'
labelDesc='고유한 커스텀 URL을 설정해주세요.' labelDesc='고유한 커스텀 URL을 설정해주세요.'
error={errors.vanity && touched.vanity ? errors.vanity : null} error={errors.vanity && touched.vanity ? errors.vanity : null}
> >
<div className='flex items-center'> <div className='flex items-center'>
koreanbots.dev/bots/ koreanbots.dev/bots/
<Input name='vanity' placeholder='koreanbots' /> <Input name='vanity' placeholder='koreanbots' />
</div> </div>
</Label> </Label>
<Label <Label
For='banner' For='banner'
@ -308,9 +327,36 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
> >
<Input name='bg' placeholder='https://koreanbots.dev/logo.png' /> <Input name='bg' placeholder='https://koreanbots.dev/logo.png' />
</Label> </Label>
</> </>
) )}
} <Divider />
<Label
For='enforcements'
label='필수 고지 내용'
labelDesc='내용에 해당하는 경우 필수로 선택해야 합니다.'
required
error={
errors.enforcements && touched.enforcements ? (errors.enforcements as string) : null
}
>
<Selects
options={Object.entries(botEnforcements).map(([k, v]) => ({
label: k,
value: v,
}))}
handleChange={(value) => {
setFieldValue(
'enforcements',
value.map((v) => v.value)
)
}}
handleTouch={() => setFieldTouched('enforcements', true)}
values={values.enforcements ?? ([] as string[])}
setValues={(value) => {
setFieldValue('enforcements', value)
}}
/>
</Label>
<Divider /> <Divider />
<p className='mb-5 mt-2 text-base'> <p className='mb-5 mt-2 text-base'>
<span className='font-semibold text-red-500'> *</span> = <span className='font-semibold text-red-500'> *</span> =
@ -320,7 +366,6 @@ const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }
<i className='far fa-save' /> <i className='far fa-save' />
</> </>
</Button> </Button>
</Form> </Form>
)} )}
</Formik> </Formik>

View File

@ -9,7 +9,7 @@ import Tooltip from 'rc-tooltip'
import { SnowflakeUtil } from 'discord.js' import { SnowflakeUtil } from 'discord.js'
import { ParsedUrlQuery } from 'querystring' import { ParsedUrlQuery } from 'querystring'
import { Bot, ResponseProps, Theme, User } from '@types' import { Bot, BotEnforcementKeys, ResponseProps, Theme, User } from '@types'
import { git, KoreanbotsEndPoints, reportCats, Status } from '@utils/Constants' import { git, KoreanbotsEndPoints, reportCats, Status } from '@utils/Constants'
import { get } from '@utils/Query' import { get } from '@utils/Query'
@ -116,13 +116,34 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
. .
</p> </p>
</Message> </Message>
) : data.enforcements.length > 0 ? (
<Message type='warning'>
<h2 className='text-lg font-extrabold'> .</h2>
<p>
{Object.entries({
JOIN_ENFORCED:
'봇의 핵심 기능은 봇의 디스코드 서버에 참여해야 사용할 수 있습니다.',
JOIN_PARTIALLY_ENFORCED:
'봇의 일부 명령어는 봇의 디스코드 서버에 참여해야 사용할 수 있습니다.',
})
.filter((el) => data.enforcements.includes(el[0] as BotEnforcementKeys))
.map(([k, v]) => (
<li key={k}>{v}</li>
))}
</p>
</Message>
) : ( ) : (
'' ''
)} )}
</div> </div>
<div className='w-full lg:flex'> <div className='w-full lg:flex'>
<div className='w-full text-center lg:w-2/12'> <div className='w-full text-center lg:w-2/12'>
<DiscordAvatar userID={data.id} size={256} className='w-full rounded-full' hash={data.avatar}/> <DiscordAvatar
userID={data.id}
size={256}
className='w-full rounded-full'
hash={data.avatar}
/>
</div> </div>
<div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'> <div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'>
<Tag <Tag
@ -158,7 +179,7 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
</p> </p>
</div> </div>
<div className='w-full lg:w-1/4'> <div className='w-full lg:w-1/4'>
{(data.state === 'ok' && !checkBotFlag(data.flags, 'private')) && ( {data.state === 'ok' && !checkBotFlag(data.flags, 'private') && (
<LongButton newTab href={`/bots/${router.query.id}/invite`}> <LongButton newTab href={`/bots/${router.query.id}/invite`}>
<h4 className='whitespace-nowrap'> <h4 className='whitespace-nowrap'>
<i className='fas fa-user-plus text-discord-blurple' /> <i className='fas fa-user-plus text-discord-blurple' />

View File

@ -1,9 +1,12 @@
import { botEnforcements } from '@utils/Constants'
import type { GuildFeature } from 'discord.js' import type { GuildFeature } from 'discord.js'
import type { IncomingMessage } from 'http' import type { IncomingMessage } from 'http'
import type { NextPageContext } from 'next' import type { NextPageContext } from 'next'
export type Nullable<T> = T | null export type Nullable<T> = T | null
export type ValueOf<T> = T[keyof T]
export interface Bot { export interface Bot {
id: string id: string
name: string name: string
@ -25,11 +28,14 @@ export interface Bot {
url: string | null url: string | null
discord: string | null discord: string | null
vanity: string | null vanity: string | null
enforcements: BotEnforcementKeys[]
bg: string bg: string
banner: string banner: string
owners: User[] | string[] owners: User[] | string[]
} }
export type BotEnforcementKeys = ValueOf<typeof botEnforcements>
export interface RawGuild { export interface RawGuild {
id: string id: string
name: string name: string

View File

@ -120,6 +120,13 @@ export const botCategoryDescription = {
: '게임 "마인크래프트"에 관련된 기능을 다룹니다.', : '게임 "마인크래프트"에 관련된 기능을 다룹니다.',
} }
export const botEnforcements = {
'서버 참여가 필요한 기능이 있습니다': 'JOIN_PARTIALLY_ENFORCED',
'서버 참여 없이는 봇의 핵심 기능을 사용할 수 없습니다': 'JOIN_ENFORCED',
'유료 구매가 필요한 기능이 있습니다': 'LICENSE_PARTIALLY_ENFORCED',
'유료 구매 없이는 봇의 핵심 기능을 사용할 수 없습니다': 'LICENSE_ENFORCED',
} as const
export const botCategoryIcon = { export const botCategoryIcon = {
: 'fas fa-cogs', : 'fas fa-cogs',
: 'fas fa-music', : 'fas fa-music',

View File

@ -62,6 +62,7 @@ async function getBot(id: string, topLevel = true): Promise<Bot> {
'bots.status', 'bots.status',
'bots.trusted', 'bots.trusted',
'bots.partnered', 'bots.partnered',
'bots.enforcements',
'bots.discord', 'bots.discord',
'bots.state', 'bots.state',
'bots.vanity', 'bots.vanity',
@ -102,7 +103,7 @@ async function getBot(id: string, topLevel = true): Promise<Bot> {
res.owners = JSON.parse(res.owners) res.owners = JSON.parse(res.owners)
res.banner = res.banner ? camoUrl(res.banner) : null res.banner = res.banner ? camoUrl(res.banner) : null
res.bg = res.bg ? camoUrl(res.bg) : null res.bg = res.bg ? camoUrl(res.bg) : null
res.enforcements = JSON.parse(res.enforcements ?? '"[]"')
if (discordBot.flags.bitfield & UserFlags.BotHTTPInteractions) { if (discordBot.flags.bitfield & UserFlags.BotHTTPInteractions) {
res.status = 'online' res.status = 'online'
} else if (botMember) { } else if (botMember) {
@ -448,6 +449,7 @@ async function getBotSubmit(id: string, date: number): Promise<SubmittedBot> {
'id', 'id',
'date', 'date',
'category', 'category',
'enforcements',
'lib', 'lib',
'prefix', 'prefix',
'intro', 'intro',
@ -463,6 +465,7 @@ async function getBotSubmit(id: string, date: number): Promise<SubmittedBot> {
.where({ id, date }) .where({ id, date })
if (res.length === 0) return null if (res.length === 0) return null
res[0].category = JSON.parse(res[0].category) res[0].category = JSON.parse(res[0].category)
res[0].enforcements = JSON.parse(res[0].enforcements || '"[]"')
res[0].owner = await get.user.load(res[0].owner) res[0].owner = await get.user.load(res[0].owner)
return res[0] return res[0]
} }
@ -474,6 +477,7 @@ async function getBotSubmits(id: string): Promise<SubmittedBot[]> {
'id', 'id',
'date', 'date',
'category', 'category',
'enforcements',
'lib', 'lib',
'prefix', 'prefix',
'intro', 'intro',
@ -492,6 +496,7 @@ async function getBotSubmits(id: string): Promise<SubmittedBot[]> {
res = await Promise.all( res = await Promise.all(
res.map(async (el) => { res.map(async (el) => {
el.category = JSON.parse(el.category) el.category = JSON.parse(el.category)
el.enforcements = JSON.parse(el.enforcements)
el.owner = owner el.owner = owner
return el return el
}) })
@ -636,6 +641,7 @@ async function submitBot(
git: data.git, git: data.git,
url: data.url, url: data.url,
category: JSON.stringify(data.category), category: JSON.stringify(data.category),
enforcements: JSON.stringify(data.enforcements),
discord: data.discord, discord: data.discord,
state: 0, state: 0,
}) })
@ -745,6 +751,7 @@ async function updateBot(id: string, data: ManageBot): Promise<number> {
intro: data.intro, intro: data.intro,
desc: data.desc, desc: data.desc,
vanity: data.vanity, vanity: data.vanity,
enforcements: JSON.stringify(data.enforcements),
banner: data.banner, banner: data.banner,
bg: data.bg, bg: data.bg,
}) })
@ -1134,6 +1141,7 @@ async function approveBotSubmission(id: string, date: number) {
'id', 'id',
'date', 'date',
'category', 'category',
'enforcements',
'lib', 'lib',
'prefix', 'prefix',
'intro', 'intro',
@ -1160,6 +1168,7 @@ async function approveBotSubmission(id: string, date: number) {
web: data.web, web: data.web,
git: data.git, git: data.git,
category: data.category, category: data.category,
enforcements: data.enforcements,
discord: data.discord, discord: data.discord,
token: sign({ id }), token: sign({ id }),
}) })

View File

@ -3,6 +3,7 @@ import YupKorean from 'yup-locales-ko'
import { ListType } from '@types' import { ListType } from '@types'
import { import {
botCategories, botCategories,
botEnforcements,
library, library,
reportCats, reportCats,
reservedVanityBypass, reservedVanityBypass,
@ -174,6 +175,7 @@ export const AddBotSubmitSchema: Yup.SchemaOf<AddBotSubmit> = Yup.object({
.min(100, '봇 설명은 최소 100자여야합니다.') .min(100, '봇 설명은 최소 100자여야합니다.')
.max(1500, '봇 설명은 최대 1500자여야합니다.') .max(1500, '봇 설명은 최대 1500자여야합니다.')
.required('봇 설명은 필수 항목입니다.'), .required('봇 설명은 필수 항목입니다.'),
enforcements: Yup.array(Yup.string().oneOf(Object.values(botEnforcements))),
_csrf: Yup.string().required(), _csrf: Yup.string().required(),
_captcha: Yup.string().required(), _captcha: Yup.string().required(),
}) })
@ -190,6 +192,7 @@ export interface AddBotSubmit {
category: string | string[] category: string | string[]
intro: string intro: string
desc: string desc: string
enforcements: string[]
_csrf: string _csrf: string
_captcha: string _captcha: string
} }
@ -303,6 +306,7 @@ export function getManageBotSchema(perkAvailable = false) {
.min(100, '봇 설명은 최소 100자여야합니다.') .min(100, '봇 설명은 최소 100자여야합니다.')
.max(1500, '봇 설명은 최대 1500자여야합니다.') .max(1500, '봇 설명은 최대 1500자여야합니다.')
.required('봇 설명은 필수 항목입니다.'), .required('봇 설명은 필수 항목입니다.'),
enforcements: Yup.array(Yup.string().oneOf(Object.values(botEnforcements))),
_csrf: Yup.string().required(), _csrf: Yup.string().required(),
} }
@ -349,6 +353,7 @@ export interface ManageBot {
desc: string desc: string
vanity: string vanity: string
banner: string banner: string
enforcements: string[]
bg: string bg: string
_csrf: string _csrf: string
} }