From 04fb5709a167893f3054eb4889a6ee495975084f Mon Sep 17 00:00:00 2001 From: aronmal Date: Wed, 27 Mar 2024 00:35:30 +0100 Subject: [PATCH] fix: caching to prevent rate limiting --- src/drizzle/schema.ts | 6 ++ src/lib/cachedDiscord.ts | 124 ++++++++++++++++++++++++ src/routes/api/auth/callback/discord.ts | 8 +- src/routes/config/[guildId].tsx | 40 +------- src/routes/config/index.tsx | 22 +---- 5 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 src/lib/cachedDiscord.ts diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 5783eeb..80564e0 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -11,6 +11,12 @@ import { varchar, } from "drizzle-orm/pg-core"; +export const cache = pgTable("cache", { + key: varchar("key").primaryKey(), + value: varchar("value").notNull(), + createdAt: timestamp("created_at").notNull(), +}); + export const users = pgTable("user", { id: varchar("id", { length: 24 }).primaryKey(), discord_id: text("discord_id").notNull(), diff --git a/src/lib/cachedDiscord.ts b/src/lib/cachedDiscord.ts new file mode 100644 index 0000000..c117d9a --- /dev/null +++ b/src/lib/cachedDiscord.ts @@ -0,0 +1,124 @@ +import { and, eq, gt } from "drizzle-orm"; +import createClient from "openapi-fetch"; +import db from "~/drizzle"; +import { cache } from "~/drizzle/schema"; +import { paths } from "~/types/discord"; +import getAccessToken from "./accessToken"; + +type Guilds = + paths["/users/@me/guilds"]["get"]["responses"]["200"]["content"]["application/json"]; + +type Channels = + paths["/guilds/{guild_id}/channels"]["get"]["responses"]["200"]["content"]["application/json"]; + +export const { GET: discordApi } = createClient({ + baseUrl: "https://discord.com/api/v10", +}); + +export async function userGuilds(userId: string) { + const path = "/users/@me/guilds"; + const key = `${path}:${userId}`; + const consoleKey = `${path.green}:${userId.yellow}`; + + const tokens = await getAccessToken(userId); + if (!tokens) { + console.log("No discord access token!"); + return null; + } + + const currentDate = new Date(); + const fifteenMinutesAgo = new Date(currentDate.getTime() - 15 * 60 * 1000); + + let guilds: Guilds | undefined = JSON.parse( + ( + await db.query.cache + .findFirst({ + where: and( + eq(cache.key, key), + gt(cache.createdAt, fifteenMinutesAgo), + ), + }) + .execute() + )?.value ?? "null", + ); + + if (!guilds) { + const { data, error } = await discordApi(path, { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }); + + if (error) console.log("Discord api error:", { error }); + + guilds = data; + + const set = { + key, + value: JSON.stringify(data), + createdAt: currentDate, + }; + await db.insert(cache).values(set).onConflictDoUpdate({ + set, + target: cache.key, + }); + console.log("To cache written.", consoleKey); + } else { + console.log("Cache value used!", consoleKey); + } + + return guilds; +} + +export async function guildChannels(guildId: bigint) { + const path = "/guilds/{guild_id}/channels"; + const key = `${path}:${String(guildId)}`; + const consoleKey = `${path.green}:${String(guildId).yellow}`; + + const currentDate = new Date(); + const fifteenMinutesAgo = new Date(currentDate.getTime() - 15 * 60 * 1000); + + let channels: Channels | undefined = JSON.parse( + ( + await db.query.cache + .findFirst({ + where: and( + eq(cache.key, key), + gt(cache.createdAt, fifteenMinutesAgo), + ), + }) + .execute() + )?.value ?? "null", + ); + + if (!channels) { + const { data, error } = await discordApi(path, { + params: { + path: { + guild_id: String(guildId), + }, + }, + headers: { + Authorization: `Bot ${import.meta.env.VITE_DISCORD_BOT_TOKEN}`, + }, + }); + + if (error) console.log("Discord api error:", { error }); + + channels = data; + + const set = { + key, + value: JSON.stringify(data), + createdAt: currentDate, + }; + await db.insert(cache).values(set).onConflictDoUpdate({ + set, + target: cache.key, + }); + + console.log("To cache written.", consoleKey); + } else { + console.log("Cache value used!", consoleKey); + } + + return channels; +} diff --git a/src/routes/api/auth/callback/discord.ts b/src/routes/api/auth/callback/discord.ts index 36db3b9..b630d8c 100644 --- a/src/routes/api/auth/callback/discord.ts +++ b/src/routes/api/auth/callback/discord.ts @@ -3,12 +3,11 @@ import { APIEvent } from "@solidjs/start/server"; import { OAuth2RequestError } from "arctic"; import { eq } from "drizzle-orm"; import httpStatus from "http-status"; -import createClient from "openapi-fetch"; import { getCookie, setCookie } from "vinxi/http"; import db from "~/drizzle"; import { discordTokens, users } from "~/drizzle/schema"; import { discord, lucia } from "~/lib/auth"; -import { paths } from "~/types/discord"; +import { discordApi } from "~/lib/cachedDiscord"; export async function GET(event: APIEvent): Promise { const url = new URL(event.request.url); @@ -39,10 +38,7 @@ export async function GET(event: APIEvent): Promise { try { const tokens = await discord.validateAuthorizationCode(code); - const { GET } = createClient({ - baseUrl: "https://discord.com/api/v10", - }); - const discordUserResponse = await GET("/users/@me", { + const discordUserResponse = await discordApi("/users/@me", { headers: { Authorization: `Bearer ${tokens.accessToken}` }, }); if (discordUserResponse.error) throw discordUserResponse.error; diff --git a/src/routes/config/[guildId].tsx b/src/routes/config/[guildId].tsx index db0e5e5..5f1ab80 100644 --- a/src/routes/config/[guildId].tsx +++ b/src/routes/config/[guildId].tsx @@ -3,7 +3,6 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"; import { eq } from "drizzle-orm"; import { PgUpdateSetSource } from "drizzle-orm/pg-core"; import moment from "moment-timezone"; -import createClient from "openapi-fetch"; import { For, Index, @@ -20,10 +19,9 @@ import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"; import Layout from "~/components/Layout"; import db from "~/drizzle"; import { guilds } from "~/drizzle/schema"; -import getAccessToken from "~/lib/accessToken"; +import { guildChannels, userGuilds } from "~/lib/cachedDiscord"; import { combineInterval, splitInterval } from "~/lib/responseBuilders"; import { zodBigIntId } from "~/lib/zod"; -import { paths } from "~/types/discord"; import "../../styles/pages/config.scss"; if (typeof import.meta.env.VITE_DISCORD_BOT_TOKEN === "undefined") @@ -65,42 +63,10 @@ const getPayload = async ( const event = getRequestEvent(); if (!event) return { success: false, message: "No request event!" }; - const pathname = new URL(event.request.url).pathname; const { user } = event.nativeEvent.context; if (!user) return { success: false, message: "User not logged in!" }; - const tokens = await getAccessToken(user.id); - if (!tokens) return { success: false, message: "No discord access token!" }; - - const { GET } = createClient({ - baseUrl: "https://discord.com/api/v10", - }); - const { data: guildsData, error } = await GET("/users/@me/guilds", { - headers: { Authorization: `Bearer ${tokens.accessToken}` }, - }); - const { data: channelsData, error: error2 } = await GET( - "/guilds/{guild_id}/channels", - { - params: { - path: { - guild_id: String(guildId), - }, - }, - headers: { - Authorization: `Bot ${import.meta.env.VITE_DISCORD_BOT_TOKEN}`, - }, - }, - ); - - if (error || error2) { - console.log("Discord api error:", { error, error2 }); - console.log(error, error2, pathname); - return { - success: false, - message: "Error on one of the discord api requests!", - }; - } - + const guildsData = await userGuilds(user.id); const guild = guildsData?.find((e) => e.id === String(guildId)); if (!guild) @@ -115,6 +81,8 @@ const getPayload = async ( "User is no MANAGE_GUILD permissions on this guild with requested id!", }; + const channelsData = await guildChannels(guildId); + const channels: { id: string; name: string; diff --git a/src/routes/config/index.tsx b/src/routes/config/index.tsx index b2ab27d..adcadde 100644 --- a/src/routes/config/index.tsx +++ b/src/routes/config/index.tsx @@ -1,14 +1,12 @@ import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons"; import { useLocation, useNavigate } from "@solidjs/router"; -import createClient from "openapi-fetch"; import { For, Show, createResource } from "solid-js"; import { getRequestEvent } from "solid-js/web"; import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"; import Layout from "~/components/Layout"; import db from "~/drizzle"; import { guilds } from "~/drizzle/schema"; -import getAccessToken from "~/lib/accessToken"; -import { paths } from "~/types/discord"; +import { userGuilds } from "~/lib/cachedDiscord"; import "../../styles/pages/config.scss"; if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined") @@ -38,31 +36,17 @@ const getPayload = async (): Promise< const event = getRequestEvent(); if (!event) return { success: false, message: "No request event!" }; - const pathname = new URL(event.request.url).pathname; const { user } = event.nativeEvent.context; if (!user) return { success: false, message: "User not logged in!" }; - const tokens = await getAccessToken(user.id); - if (!tokens) return { success: false, message: "No discord access token!" }; - - const { GET } = createClient({ - baseUrl: "https://discord.com/api/v10", - }); - const { data, error } = await GET("/users/@me/guilds", { - headers: { Authorization: `Bearer ${tokens.accessToken}` }, - }); const configs = await db.select().from(guilds).execute(); - if (error) { - console.log("Discord api error:", { error }); - return { success: false, message: "Error on discord api request!" }; - } - console.log(pathname, "success"); + const guildData = await userGuilds(user.id); const joined: Guild[] = []; const canJoin: Guild[] = []; - data + guildData ?.filter((e) => parseInt(e.permissions) & (1 << 5)) .forEach(({ id, name, icon }) => { configs.map((e) => e.id.toString()).includes(id)