diff --git a/pages/api/v2/bots/[id]/vote.ts b/pages/api/v2/bots/[id]/vote.ts new file mode 100644 index 0000000..d3bc2e0 --- /dev/null +++ b/pages/api/v2/bots/[id]/vote.ts @@ -0,0 +1,35 @@ +import { NextApiRequest } from 'next' + +import { CaptchaVerify, get, put } from '@utils/Query' +import RequestHandler from '@utils/RequestHandler' +import ResponseWrapper from '@utils/ResponseWrapper' +import { checkToken } from '@utils/Csrf' + +const BotVote = RequestHandler() + .post(async (req: PostApiRequest, res) => { + const user = await get.Authorization(req.cookies.token) + if(!user) return ResponseWrapper(res, { code: 401 }) + const bot = await get.bot.load(req.query.id) + if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) + const csrfValidated = checkToken(req, res, req.body._csrf) + if (!csrfValidated) return + const captcha = await CaptchaVerify(req.body._captcha) + if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' }) + + const vote = await put.voteBot(user, bot.id) + if(vote === null) return ResponseWrapper(res, { code: 401 }) + else if(vote === true) return ResponseWrapper(res, { code: 200 }) + else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } }) + }) + +interface PostApiRequest extends NextApiRequest { + query: { + id: string + } + body: { + _captcha: string + _csrf: string + } +} + +export default BotVote \ No newline at end of file diff --git a/pages/bots/[id]/vote.tsx b/pages/bots/[id]/vote.tsx index ed46dcf..24253f0 100644 --- a/pages/bots/[id]/vote.tsx +++ b/pages/bots/[id]/vote.tsx @@ -3,9 +3,9 @@ import Link from 'next/link' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' -import { Bot, CsrfContext, Theme, User } from '@types' +import { Bot, CsrfContext, ResponseProps, Theme, User } from '@types' import { get } from '@utils/Query' -import { makeBotURL, parseCookie, checkBotFlag } from '@utils/Tools' +import { makeBotURL, parseCookie, checkBotFlag, redirectTo } from '@utils/Tools' import { ParsedUrlQuery } from 'querystring' @@ -13,6 +13,9 @@ import NotFound from 'pages/404' import { getToken } from '@utils/Csrf' import Captcha from '@components/Captcha' import { useState } from 'react' +import Fetch from '@utils/Fetch' +import Day from '@utils/Day' +import { getJosaPicker } from 'josa' const Container = dynamic(() => import('@components/Container')) @@ -23,14 +26,20 @@ const Segment = dynamic(() => import('@components/Segment')) const SEO = dynamic(() => import('@components/SEO')) const Advertisement = dynamic(() => import('@components/Advertisement')) -const VoteBot: NextPage = ({ data, csrfToken , theme}) => { +const VoteBot: NextPage = ({ data, user, csrfToken, theme }) => { const [ votingStatus, setVotingStatus ] = useState(0) + const [ result, setResult ] = useState>(null) const router = useRouter() if(!data?.id) return if(csrfToken !== router.query.csrfToken) { router.push(`/bots/${data.id}`) return <> } + if(!user) { + localStorage.redirectTo = window.location.href + redirectTo(router, 'login') + return + } if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== router.query.id) router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`) return = ({ data, csrfToken , theme}) => { } /> - {data.name}으로 돌아가기 + {data.name}{getJosaPicker('로')(data.name)} 돌아가기
{data.votes}} dark />

{data.name}

-

12시간 뒤에 다시 투표하실 수 있습니다.

+

12시간 마다 다시 투표하실 수 있습니다.

{ votingStatus === 0 ? - : votingStatus === 1 ? setVotingStatus(2)}/> - :

해당 봇에 투표했습니다!

+ : votingStatus === 1 ? { + const res = await Fetch<{ retryAfter: number }|unknown>(`/bots/${data.id}/vote`, { method: 'POST', body: JSON.stringify({ _csrf: csrfToken, _captcha: key }) }) + setResult(res) + setVotingStatus(2) + }} + /> + : result.code === 200 ?

해당 봇에 투표했습니다!

+ : result.code === 429 ? <> +

이미 해당 봇에 투표하였습니다.

+

{Day(+new Date() + result.data?.retryAfter).fromNow()} 다시 투표하실 수 있습니다.

+ + :

{result.message}

}
diff --git a/utils/Constants.ts b/utils/Constants.ts index 25cfbe1..06a302b 100644 --- a/utils/Constants.ts +++ b/utils/Constants.ts @@ -2,6 +2,7 @@ import { Bot, ImageOptions, KoreanbotsImageOptions } from '@types' import { KeyMap } from 'react-hotkeys' import { formatNumber, makeImageURL } from './Tools' +export const VOTE_COOLDOWN = 12 * 60 * 60 * 1000 export const Status = { online: { text: '온라인', diff --git a/utils/Query.ts b/utils/Query.ts index e8d0a68..5df1259 100644 --- a/utils/Query.ts +++ b/utils/Query.ts @@ -4,7 +4,7 @@ import DataLoader from 'dataloader' import { User as DiscordUser } from 'discord.js' import { Bot, User, ListType, BotList, TokenRegister, BotFlags, DiscordUserFlags, SubmittedBot } from '@types' -import { categories, imageSafeHost, SpecialEndPoints } from './Constants' +import { categories, imageSafeHost, SpecialEndPoints, VOTE_COOLDOWN } from './Constants' import knex from './Knex' import { DiscordBot, getMainGuild } from './DiscordBot' @@ -181,6 +181,33 @@ async function getBotSubmits(id: string) { return res } +/** + * @param userID + * @param botID + * @returns Timestamp + */ +async function getBotVote(userID: string, botID: string): Promise { + const user = await knex('users').select(['votes']).where({ id: userID }) + if(user.length === 0) return null + const data = JSON.parse(user[0].votes) + return data[botID] || 0 + + +} + +async function voteBot(userID: string, botID: string): Promise { + const user = await knex('users').select(['votes']).where({ id: userID }) + if(user.length === 0) return null + const date = +new Date() + const data = JSON.parse(user[0].votes) + const lastDate = data[botID] || 0 + if(date - lastDate < VOTE_COOLDOWN) return VOTE_COOLDOWN - (date - lastDate) + data[botID] = date + await knex('bots').where({ id: botID }).increment('votes', 1) + await knex('users').where({ id: userID }).update({ votes: JSON.stringify(data) }) + return true +} + /** * @returns 1 - Has pending Bots * @returns 2 - Already submitted ID @@ -339,8 +366,6 @@ export async function CaptchaVerify(response: string): Promise { }) }).then(r=> r.json()) - console.log(res) - return res.success } @@ -425,6 +450,7 @@ export const get = { (await Promise.all(urls.map(async (url: string) => await getImage(url)))) , { cacheMap: new TLRU({ maxStoreSize: 500, maxAgeMs: 3600000 }) }), }, + botVote: getBotVote, Authorization, BotAuthorization } @@ -439,6 +465,7 @@ export const update = { } export const put = { + voteBot, submitBot }