-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduces discord invite page
- Loading branch information
Showing
8 changed files
with
237 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
<script setup lang="ts"> | ||
import { get, set } from '@vueuse/core'; | ||
import { type DiscordInvite, DiscordInviteResponse } from '~/types/discord'; | ||
import { commonAttrs } from '~/utils/metadata'; | ||
const { t } = useI18n(); | ||
const valid = ref(true); | ||
const invite = ref<DiscordInvite>(); | ||
const error = ref(); | ||
const inviteLink = computed(() => { | ||
if (!isDefined(invite)) { | ||
return null; | ||
} | ||
return `https://discord.com/invite/${get(invite).code}`; | ||
}); | ||
const expiry = computed(() => { | ||
if (!isDefined(invite)) { | ||
return null; | ||
} | ||
return get(invite, 'expires_at'); | ||
}); | ||
const { onSuccess, onError, onExpired, captchaId, resetCaptcha } = | ||
useRecaptcha(); | ||
const onCaptchaSuccess = async (token: string) => { | ||
onSuccess(token); | ||
try { | ||
const response = await useFetch('/_api/discord_invite', { | ||
method: 'POST', | ||
body: { | ||
captcha: token, | ||
}, | ||
}); | ||
set(invite, DiscordInviteResponse.parse(get(response.data))); | ||
} catch (e: any) { | ||
logger.error(e); | ||
resetCaptcha(); | ||
set(error, e.message); | ||
set(invite, undefined); | ||
} | ||
}; | ||
useHead({ | ||
title: 'join our discord', | ||
meta: [ | ||
{ | ||
key: 'description', | ||
name: 'description', | ||
content: "Join rotki's discord community", | ||
}, | ||
], | ||
...commonAttrs(), | ||
}); | ||
const css = useCssModule(); | ||
</script> | ||
|
||
<template> | ||
<div :class="css.container"> | ||
<div> | ||
<TextHeading> {{ t('discord.title') }} </TextHeading> | ||
<TextParagraph v-show="!inviteLink"> | ||
{{ t('discord.description') }} | ||
</TextParagraph> | ||
<div v-if="inviteLink"> | ||
<i18n-t tag="div" keypath="discord.invite.link"> | ||
<template #link> | ||
<ExternalLink :url="inviteLink" no-ref> | ||
{{ inviteLink }} | ||
</ExternalLink> | ||
</template> | ||
</i18n-t> | ||
<i18n-t v-if="expiry" tag="div" keypath="discord.invite.expiry"> | ||
<template #expiry> | ||
<span class="font-bold">{{ expiry.toLocaleString() }}</span> | ||
</template> | ||
</i18n-t> | ||
</div> | ||
<Recaptcha | ||
v-else | ||
:invalid="!valid" | ||
@error="onError()" | ||
@expired="onExpired()" | ||
@success="onCaptchaSuccess($event)" | ||
@captcha-id="captchaId = $event" | ||
/> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<style module> | ||
.container { | ||
@apply flex flex-row items-center justify-center h-screen w-screen; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { FetchError } from 'ofetch'; | ||
import { $fetch } from 'ofetch/node'; | ||
import { | ||
CaptchaVerification, | ||
type DiscordInvite, | ||
DiscordInviteBody, | ||
DiscordInviteResponse, | ||
} from '~/types/discord'; | ||
import { discordRequest } from '~/utils/discord'; | ||
|
||
export default defineEventHandler(async (event) => { | ||
const { | ||
recaptchaSecret, | ||
discord: { token, channelId }, | ||
} = useRuntimeConfig(); | ||
const requestBody = await readBody(event); | ||
const body = DiscordInviteBody.parse(requestBody); | ||
|
||
const response = CaptchaVerification.parse( | ||
await $fetch('https://www.google.com/recaptcha/api/siteverify', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
}, | ||
body: new URLSearchParams( | ||
Object.entries({ | ||
secret: recaptchaSecret, | ||
response: body.captcha, | ||
}), | ||
).toString(), | ||
}), | ||
); | ||
|
||
if (!response.success) { | ||
throw createError({ | ||
statusCode: 400, | ||
statusMessage: response['error-codes']?.join(',') ?? '', | ||
}); | ||
} | ||
|
||
try { | ||
const discordResponse = await discordRequest<DiscordInvite>( | ||
`/channels/${channelId}/invites`, | ||
{ | ||
method: 'POST', | ||
// https://discord.com/developers/docs/resources/channel#create-channel-invite | ||
body: { | ||
max_age: 1800, | ||
max_uses: 1, | ||
unique: true, | ||
}, | ||
}, | ||
token, | ||
); | ||
|
||
return DiscordInviteResponse.parse(discordResponse); | ||
} catch (e: any) { | ||
if (e instanceof FetchError) { | ||
throw createError({ | ||
statusCode: e.statusCode, | ||
statusMessage: e.statusMessage, | ||
}); | ||
} | ||
|
||
throw createError({ | ||
statusCode: 500, | ||
statusMessage: e.message, | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { z } from 'zod'; | ||
|
||
export const DiscordInviteBody = z.object({ | ||
captcha: z.string(), | ||
}); | ||
|
||
export const CaptchaVerification = z.object({ | ||
success: z.boolean(), | ||
challenge_ts: z.string().optional(), | ||
hostname: z.string().optional(), | ||
'error-codes': z.array(z.string()).optional(), | ||
}); | ||
|
||
export type CaptchaVerification = z.infer<typeof CaptchaVerification>; | ||
|
||
export const DiscordInviteResponse = z.object({ | ||
code: z.string(), | ||
expires_at: z.coerce.date(), | ||
}); | ||
|
||
export type DiscordInvite = z.infer<typeof DiscordInviteResponse>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { $fetch, type FetchOptions, type FetchRequest } from 'ofetch/node'; | ||
|
||
export async function discordRequest< | ||
Resp, | ||
Req extends FetchRequest = FetchRequest, | ||
>(endpoint: Req, options: FetchOptions<'json'>, token: string): Promise<Resp> { | ||
const url = `https://discord.com/api/v10/${endpoint}`; | ||
if (options.body) { | ||
options.body = JSON.stringify(options.body); | ||
} | ||
return await $fetch<Resp>(url, { | ||
headers: { | ||
Authorization: `Bot ${token}`, | ||
'Content-Type': 'application/json; charset=UTF-8', | ||
'User-Agent': 'RotkiBot', | ||
}, | ||
...options, | ||
}); | ||
} |