mirror of
https://github.com/koreanbots/core.git
synced 2025-12-15 14:10:22 +00:00
feat: added vote
This commit is contained in:
parent
9cf3e06709
commit
05b92d1980
35
pages/api/v2/bots/[id]/vote.ts
Normal file
35
pages/api/v2/bots/[id]/vote.ts
Normal 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
|
||||||
@ -3,9 +3,9 @@ import Link from 'next/link'
|
|||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useRouter } from 'next/router'
|
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 { get } from '@utils/Query'
|
||||||
import { makeBotURL, parseCookie, checkBotFlag } from '@utils/Tools'
|
import { makeBotURL, parseCookie, checkBotFlag, redirectTo } from '@utils/Tools'
|
||||||
|
|
||||||
import { ParsedUrlQuery } from 'querystring'
|
import { ParsedUrlQuery } from 'querystring'
|
||||||
|
|
||||||
@ -13,6 +13,9 @@ import NotFound from 'pages/404'
|
|||||||
import { getToken } from '@utils/Csrf'
|
import { getToken } from '@utils/Csrf'
|
||||||
import Captcha from '@components/Captcha'
|
import Captcha from '@components/Captcha'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import Fetch from '@utils/Fetch'
|
||||||
|
import Day from '@utils/Day'
|
||||||
|
import { getJosaPicker } from 'josa'
|
||||||
|
|
||||||
|
|
||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
@ -23,14 +26,20 @@ const Segment = dynamic(() => import('@components/Segment'))
|
|||||||
const SEO = dynamic(() => import('@components/SEO'))
|
const SEO = dynamic(() => import('@components/SEO'))
|
||||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
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 [ votingStatus, setVotingStatus ] = useState(0)
|
||||||
|
const [ result, setResult ] = useState<ResponseProps<{retryAfter?: number}>>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if(!data?.id) return <NotFound />
|
if(!data?.id) return <NotFound />
|
||||||
if(csrfToken !== router.query.csrfToken) {
|
if(csrfToken !== router.query.csrfToken) {
|
||||||
router.push(`/bots/${data.id}`)
|
router.push(`/bots/${data.id}`)
|
||||||
return <></>
|
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}`)
|
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'>
|
return <Container paddingTop className='py-10'>
|
||||||
<SEO title={data.name} description={`한국 디스코드봇 리스트에서 ${data.name}에 투표하세요`} image={
|
<SEO title={data.name} description={`한국 디스코드봇 리스트에서 ${data.name}에 투표하세요`} image={
|
||||||
@ -40,21 +49,31 @@ const VoteBot: NextPage<VoteBotProps> = ({ data, csrfToken , theme}) => {
|
|||||||
} />
|
} />
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<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>으로 돌아가기</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-10 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 />
|
||||||
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1>
|
<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'>
|
<div className='inline-block mt-2'>
|
||||||
{
|
{
|
||||||
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
|
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
|
||||||
<><i className='far fa-heart text-red-600'/> 하트 추가</>
|
<><i className='far fa-heart text-red-600'/> 하트 추가</>
|
||||||
</Button>
|
</Button>
|
||||||
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={() => setVotingStatus(2)}/>
|
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={async (key) => {
|
||||||
: <h2 className='text-2xl font-bold'>해당 봇에 투표했습니다!</h2>
|
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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Bot, ImageOptions, KoreanbotsImageOptions } from '@types'
|
|||||||
import { KeyMap } from 'react-hotkeys'
|
import { KeyMap } from 'react-hotkeys'
|
||||||
import { formatNumber, makeImageURL } from './Tools'
|
import { formatNumber, makeImageURL } from './Tools'
|
||||||
|
|
||||||
|
export const VOTE_COOLDOWN = 12 * 60 * 60 * 1000
|
||||||
export const Status = {
|
export const Status = {
|
||||||
online: {
|
online: {
|
||||||
text: '온라인',
|
text: '온라인',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import DataLoader from 'dataloader'
|
|||||||
import { User as DiscordUser } from 'discord.js'
|
import { User as DiscordUser } from 'discord.js'
|
||||||
|
|
||||||
import { Bot, User, ListType, BotList, TokenRegister, BotFlags, DiscordUserFlags, SubmittedBot } from '@types'
|
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 knex from './Knex'
|
||||||
import { DiscordBot, getMainGuild } from './DiscordBot'
|
import { DiscordBot, getMainGuild } from './DiscordBot'
|
||||||
@ -181,6 +181,33 @@ async function getBotSubmits(id: string) {
|
|||||||
return res
|
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 1 - Has pending Bots
|
||||||
* @returns 2 - Already submitted ID
|
* @returns 2 - Already submitted ID
|
||||||
@ -339,8 +366,6 @@ export async function CaptchaVerify(response: string): Promise<boolean> {
|
|||||||
})
|
})
|
||||||
}).then(r=> r.json())
|
}).then(r=> r.json())
|
||||||
|
|
||||||
console.log(res)
|
|
||||||
|
|
||||||
return res.success
|
return res.success
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,6 +450,7 @@ export const get = {
|
|||||||
(await Promise.all(urls.map(async (url: string) => await getImage(url))))
|
(await Promise.all(urls.map(async (url: string) => await getImage(url))))
|
||||||
, { cacheMap: new TLRU({ maxStoreSize: 500, maxAgeMs: 3600000 }) }),
|
, { cacheMap: new TLRU({ maxStoreSize: 500, maxAgeMs: 3600000 }) }),
|
||||||
},
|
},
|
||||||
|
botVote: getBotVote,
|
||||||
Authorization,
|
Authorization,
|
||||||
BotAuthorization
|
BotAuthorization
|
||||||
}
|
}
|
||||||
@ -439,6 +465,7 @@ export const update = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const put = {
|
export const put = {
|
||||||
|
voteBot,
|
||||||
submitBot
|
submitBot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user