fix: caching to prevent rate limiting

This commit is contained in:
Aron Malcher 2024-03-27 00:35:30 +01:00
parent 136468b4bd
commit 04fb5709a1
Signed by: aronmal
GPG key ID: 816B7707426FC612
5 changed files with 139 additions and 61 deletions

View file

@ -11,6 +11,12 @@ import {
varchar, varchar,
} from "drizzle-orm/pg-core"; } 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", { export const users = pgTable("user", {
id: varchar("id", { length: 24 }).primaryKey(), id: varchar("id", { length: 24 }).primaryKey(),
discord_id: text("discord_id").notNull(), discord_id: text("discord_id").notNull(),

124
src/lib/cachedDiscord.ts Normal file
View file

@ -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<paths>({
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;
}

View file

@ -3,12 +3,11 @@ import { APIEvent } from "@solidjs/start/server";
import { OAuth2RequestError } from "arctic"; import { OAuth2RequestError } from "arctic";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import httpStatus from "http-status"; import httpStatus from "http-status";
import createClient from "openapi-fetch";
import { getCookie, setCookie } from "vinxi/http"; import { getCookie, setCookie } from "vinxi/http";
import db from "~/drizzle"; import db from "~/drizzle";
import { discordTokens, users } from "~/drizzle/schema"; import { discordTokens, users } from "~/drizzle/schema";
import { discord, lucia } from "~/lib/auth"; import { discord, lucia } from "~/lib/auth";
import { paths } from "~/types/discord"; import { discordApi } from "~/lib/cachedDiscord";
export async function GET(event: APIEvent): Promise<Response> { export async function GET(event: APIEvent): Promise<Response> {
const url = new URL(event.request.url); const url = new URL(event.request.url);
@ -39,10 +38,7 @@ export async function GET(event: APIEvent): Promise<Response> {
try { try {
const tokens = await discord.validateAuthorizationCode(code); const tokens = await discord.validateAuthorizationCode(code);
const { GET } = createClient<paths>({ const discordUserResponse = await discordApi("/users/@me", {
baseUrl: "https://discord.com/api/v10",
});
const discordUserResponse = await GET("/users/@me", {
headers: { Authorization: `Bearer ${tokens.accessToken}` }, headers: { Authorization: `Bearer ${tokens.accessToken}` },
}); });
if (discordUserResponse.error) throw discordUserResponse.error; if (discordUserResponse.error) throw discordUserResponse.error;

View file

@ -3,7 +3,6 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { PgUpdateSetSource } from "drizzle-orm/pg-core"; import { PgUpdateSetSource } from "drizzle-orm/pg-core";
import moment from "moment-timezone"; import moment from "moment-timezone";
import createClient from "openapi-fetch";
import { import {
For, For,
Index, Index,
@ -20,10 +19,9 @@ import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
import Layout from "~/components/Layout"; import Layout from "~/components/Layout";
import db from "~/drizzle"; import db from "~/drizzle";
import { guilds } from "~/drizzle/schema"; import { guilds } from "~/drizzle/schema";
import getAccessToken from "~/lib/accessToken"; import { guildChannels, userGuilds } from "~/lib/cachedDiscord";
import { combineInterval, splitInterval } from "~/lib/responseBuilders"; import { combineInterval, splitInterval } from "~/lib/responseBuilders";
import { zodBigIntId } from "~/lib/zod"; import { zodBigIntId } from "~/lib/zod";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss"; import "../../styles/pages/config.scss";
if (typeof import.meta.env.VITE_DISCORD_BOT_TOKEN === "undefined") if (typeof import.meta.env.VITE_DISCORD_BOT_TOKEN === "undefined")
@ -65,42 +63,10 @@ const getPayload = async (
const event = getRequestEvent(); const event = getRequestEvent();
if (!event) return { success: false, message: "No request event!" }; if (!event) return { success: false, message: "No request event!" };
const pathname = new URL(event.request.url).pathname;
const { user } = event.nativeEvent.context; const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" }; if (!user) return { success: false, message: "User not logged in!" };
const tokens = await getAccessToken(user.id); const guildsData = await userGuilds(user.id);
if (!tokens) return { success: false, message: "No discord access token!" };
const { GET } = createClient<paths>({
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 guild = guildsData?.find((e) => e.id === String(guildId)); const guild = guildsData?.find((e) => e.id === String(guildId));
if (!guild) if (!guild)
@ -115,6 +81,8 @@ const getPayload = async (
"User is no MANAGE_GUILD permissions on this guild with requested id!", "User is no MANAGE_GUILD permissions on this guild with requested id!",
}; };
const channelsData = await guildChannels(guildId);
const channels: { const channels: {
id: string; id: string;
name: string; name: string;

View file

@ -1,14 +1,12 @@
import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons"; import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons";
import { useLocation, useNavigate } from "@solidjs/router"; import { useLocation, useNavigate } from "@solidjs/router";
import createClient from "openapi-fetch";
import { For, Show, createResource } from "solid-js"; import { For, Show, createResource } from "solid-js";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"; import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
import Layout from "~/components/Layout"; import Layout from "~/components/Layout";
import db from "~/drizzle"; import db from "~/drizzle";
import { guilds } from "~/drizzle/schema"; import { guilds } from "~/drizzle/schema";
import getAccessToken from "~/lib/accessToken"; import { userGuilds } from "~/lib/cachedDiscord";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss"; import "../../styles/pages/config.scss";
if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined") if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined")
@ -38,31 +36,17 @@ const getPayload = async (): Promise<
const event = getRequestEvent(); const event = getRequestEvent();
if (!event) return { success: false, message: "No request event!" }; if (!event) return { success: false, message: "No request event!" };
const pathname = new URL(event.request.url).pathname;
const { user } = event.nativeEvent.context; const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" }; 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<paths>({
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(); const configs = await db.select().from(guilds).execute();
if (error) { const guildData = await userGuilds(user.id);
console.log("Discord api error:", { error });
return { success: false, message: "Error on discord api request!" };
}
console.log(pathname, "success");
const joined: Guild[] = []; const joined: Guild[] = [];
const canJoin: Guild[] = []; const canJoin: Guild[] = [];
data guildData
?.filter((e) => parseInt(e.permissions) & (1 << 5)) ?.filter((e) => parseInt(e.permissions) & (1 << 5))
.forEach(({ id, name, icon }) => { .forEach(({ id, name, icon }) => {
configs.map((e) => e.id.toString()).includes(id) configs.map((e) => e.id.toString()).includes(id)