feat: added vote

This commit is contained in:
wonderlandpark 2021-05-05 12:04:43 +09:00
parent 9cf3e06709
commit 05b92d1980
No known key found for this signature in database
GPG Key ID: E3E650B146478C64
4 changed files with 92 additions and 10 deletions

View File

@ -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

View File

@ -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<VoteBotProps> = ({ data, csrfToken , theme}) => {
const VoteBot: NextPage<VoteBotProps> = ({ data, user, csrfToken, theme }) => {
const [ votingStatus, setVotingStatus ] = useState(0)
const [ result, setResult ] = useState<ResponseProps<{retryAfter?: number}>>(null)
const router = useRouter()
if(!data?.id) return <NotFound />
if(csrfToken !== router.query.csrfToken) {
router.push(`/bots/${data.id}`)
return <></>
}
if(!user) {
localStorage.redirectTo = window.location.href
redirectTo(router, 'login')
return <SEO title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드봇 리스트에 등록하세요.'/>
}
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 <Container paddingTop className='py-10'>
<SEO title={data.name} description={`한국 디스코드봇 리스트에서 ${data.name}에 투표하세요`} image={
@ -40,21 +49,31 @@ const VoteBot: NextPage<VoteBotProps> = ({ data, csrfToken , theme}) => {
} />
<Advertisement />
<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> </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>
<Segment className='mb-10 py-8'>
<div className='text-center'>
<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 />
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1>
<h4 className='text-md mt-1'>12 .</h4>
<h4 className='text-md mt-1'>12 .</h4>
<div className='inline-block mt-2'>
{
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
<><i className='far fa-heart text-red-600'/> </>
</Button>
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={() => setVotingStatus(2)}/>
: <h2 className='text-2xl font-bold'> !</h2>
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={async (key) => {
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 ? <h2 className='text-2xl font-bold'> !</h2>
: result.code === 429 ? <>
<h2 className='text-2xl font-bold'> .</h2>
<h4 className='text-md mt-1'>{Day(+new Date() + result.data?.retryAfter).fromNow()} .</h4>
</>
: <p>{result.message}</p>
}
</div>

View File

@ -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: '온라인',

View File

@ -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<number|null> {
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<number|boolean> {
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<boolean> {
})
}).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
}