fix: caching to prevent rate limiting
This commit is contained in:
parent
136468b4bd
commit
04fb5709a1
5 changed files with 139 additions and 61 deletions
|
@ -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
124
src/lib/cachedDiscord.ts
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue