Skip to content

Commit

Permalink
feat: introduces discord invite page
Browse files Browse the repository at this point in the history
  • Loading branch information
kelsos committed Jul 17, 2023
1 parent 945ec59 commit 58a955b
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ NUXT_PUBLIC_MAINTENANCE=false
NUXT_PUBLIC_TESTING=true
#PROXY_DOMAIN=rotki.com
#PROXY_INSECURE=true #When set it will proxy to http instead of https

## FOR DISCORD FUNCTIONALITY
# RECAPTCHA VERIFICATION
NUXT_RECAPTCHA_SECRET=

# DISCORD
NUXT_DISCORD_APP_ID=
NUXT_DISCORD_TOKEN=
NUXT_DISCORD_PUBLIC_KEY=
NUXT_DISCORD_GUILD_ID=
NUXT_DISCORD_CHANNEL_ID=
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
{
registeredComponentsOnly: false,
// components are only exported in kebab-case
ignores: ['i18n-t'],
ignores: ['i18n-t', 'i18n-d'],
},
],
},
Expand Down
8 changes: 8 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
"get_in_touch": "Get In Touch Now",
"go_back_home": "Go back home"
},
"discord": {
"title": "Join rotki's Discord",
"description": "You will get an invite to rotki's discord after solving the captcha below.",
"invite": {
"link": "You can use the link {link} to join our Discord.",
"expiry": "The link is valid until {expiry}."
}
},
"supported_defi": {
"title": "Dedicated DeFi support for",
"caption": "* All DeFi protocols are supported, these are just the ones we have dedicated views for."
Expand Down
8 changes: 8 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export default defineNuxtConfig({
},

runtimeConfig: {
recaptchaSecret: '',
discord: {
appId: '',
token: '',
publicKey: '',
guildId: 0,
channelId: 0,
},
public: {
recaptcha: {
siteKey: '',
Expand Down
99 changes: 99 additions & 0 deletions pages/discord.vue
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) {
logger.error(e);
resetCaptcha();
set(error, e.message);

Check failure on line 43 in pages/discord.vue

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

'e' is of type 'unknown'.
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>
72 changes: 72 additions & 0 deletions server/routes/_api/discord_invite.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { FetchError } from 'ofetch';
import { $fetch } from 'ofetch/node';
import {
CaptchaVerification,
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<CaptchaVerifyResponse>(

Check failure on line 19 in server/routes/_api/discord_invite.post.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

Cannot find name 'CaptchaVerifyResponse'.
'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(','),

Check failure on line 39 in server/routes/_api/discord_invite.post.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

Object is possibly 'undefined'.
});
}

try {
const discordResponse = await discordRequest<DiscordInviteResponse>(

Check failure on line 44 in server/routes/_api/discord_invite.post.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

'DiscordInviteResponse' refers to a value, but is being used as a type here. Did you mean 'typeof DiscordInviteResponse'?
`/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,
});
}
});
19 changes: 19 additions & 0 deletions types/discord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 const DiscordInviteResponse = z.object({
code: z.string(),
expires_at: z.coerce.date(),
});

export type DiscordInvite = z.infer<typeof DiscordInviteResponse>;
24 changes: 24 additions & 0 deletions utils/discord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type NitroFetchOptions, type NitroFetchRequest } from 'nitropack';
import { $fetch } from 'ofetch/node';

export async function discordRequest<
Resp,
Req extends NitroFetchRequest = NitroFetchRequest,
>(
endpoint: Req,
options: NitroFetchOptions<Req>,
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, {

Check failure on line 16 in utils/discord/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

Argument of type '{ method?: AvailableRouterMethod<Req> | Uppercase<AvailableRouterMethod<Req>> | undefined; baseURL?: string | undefined; ... 22 more ...; window?: null | undefined; }' is not assignable to parameter of type 'FetchOptions<"json">'.
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json; charset=UTF-8',
'User-Agent': 'RotkiBot',
},
...options,
});
}

0 comments on commit 58a955b

Please sign in to comment.