> {
- warst: string;
-}
-
export const discord = new Discord(
import.meta.env.VITE_DISCORD_CLIENT_ID,
import.meta.env.VITE_DISCORD_CLIENT_SECRET,
import.meta.env.VITE_AUTH_REDIRECT_URL,
);
+
+const unencoded = `${import.meta.env.VITE_DISCORD_CLIENT_ID}:${import.meta.env.VITE_DISCORD_CLIENT_SECRET}`;
+const encoded = btoa(unencoded);
+
+export const BasicAuth = {
+ unencoded: `Basic ${unencoded}`,
+ encoded: `Basic ${encoded}`,
+};
diff --git a/src/lib/responseBuilders.ts b/src/lib/responseBuilders.ts
new file mode 100644
index 0000000..ee58883
--- /dev/null
+++ b/src/lib/responseBuilders.ts
@@ -0,0 +1,83 @@
+import stringify from "json-stable-stringify";
+import objectHash from "object-hash";
+
+export const buildMatches = (
+ matches: {
+ id: number;
+ messageId: string;
+ guildId: string;
+ channelId: string;
+ matchType: string;
+ createrId: string;
+ roleId: string;
+ opponentName: string;
+ utc_ts: Date;
+ }[],
+) =>
+ matches.map(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ({ id, guildId, utc_ts, ...match }) => ({
+ ...match,
+ utc_ts: utc_ts.toISOString(),
+ }),
+ );
+
+export function buildConfig(guildQuery: {
+ id: string;
+ timezone: string;
+ tpEnabled: boolean;
+ tpChannelId: string | null;
+ tpInterval: number;
+ tpRoles: boolean;
+ isAvailableRoleId: string | null;
+ wantsToBeNotifieRoledId: string | null;
+ tpMessages: {
+ messageId: string | null;
+ day: number;
+ guildId: string;
+ }[];
+ matches: {
+ id: number;
+ messageId: string;
+ guildId: string;
+ channelId: string;
+ matchType: string;
+ createrId: string;
+ roleId: string;
+ opponentName: string;
+ utc_ts: Date;
+ }[];
+}) {
+ const {
+ id,
+ timezone,
+ tpEnabled,
+ tpChannelId,
+ tpInterval,
+ tpRoles,
+ isAvailableRoleId,
+ wantsToBeNotifieRoledId,
+ } = guildQuery;
+
+ const targetMinute = tpInterval & 63;
+ const targetHour = (tpInterval >> 6) & 31;
+ const targetDay = (tpInterval >> 11) & 7;
+
+ const payload = {
+ guildId: id,
+ timezone,
+ features: {
+ timePlanning: {
+ enabled: tpEnabled,
+ channelId: tpChannelId,
+ targetMinute,
+ targetHour,
+ targetDay,
+ roles: { enabled: tpRoles, isAvailableRoleId, wantsToBeNotifieRoledId },
+ },
+ },
+ matches: buildMatches(guildQuery.matches),
+ checksum: objectHash(stringify(guildQuery)),
+ };
+ return payload;
+}
diff --git a/src/lib/responses.ts b/src/lib/responses.ts
new file mode 100644
index 0000000..65d6991
--- /dev/null
+++ b/src/lib/responses.ts
@@ -0,0 +1,39 @@
+import httpStatus from "http-status";
+import {
+ APIResponse,
+ Methods,
+ MyPaths,
+ ResponseSchemas,
+ StatusCodes,
+} from "~/types/backend";
+
+export function ErrorResponse<
+ P extends MyPaths,
+ M extends Methods,
+ C extends StatusCodes
= StatusCodes
,
+>(code: C, error?: string): APIResponse
{
+ const responseData = {
+ error: error ?? httpStatus[`${httpStatus[code]}_NAME`],
+ };
+
+ console.log(responseData);
+ return new Response(JSON.stringify(responseData), {
+ status: httpStatus[code],
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+}
+
+export function Res<
+ P extends MyPaths,
+ M extends Methods
,
+ C extends StatusCodes
= StatusCodes
,
+>(code: C, payload: ResponseSchemas
): APIResponse
{
+ return new Response(payload === null ? null : JSON.stringify(payload), {
+ status: httpStatus[code],
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+}
diff --git a/src/lib/zod.ts b/src/lib/zod.ts
new file mode 100644
index 0000000..b5810e1
--- /dev/null
+++ b/src/lib/zod.ts
@@ -0,0 +1,37 @@
+import moment from "moment-timezone";
+import { z } from "zod";
+
+export const zodId = z
+ .string()
+ .refine((value) => /^\d{7,20}$/.test(value), "Invalid ID supplied");
+
+export const zodTpMessages = z.object({
+ channelId: zodId,
+ messageIds: z.object({
+ "0": zodId.nullable(),
+ "1": zodId.nullable(),
+ "2": zodId.nullable(),
+ "3": zodId.nullable(),
+ "4": zodId.nullable(),
+ "5": zodId.nullable(),
+ "6": zodId.nullable(),
+ }),
+});
+
+export const zodMatch = z.object({
+ match: z.object({
+ channelId: zodId,
+ createrId: zodId,
+ messageId: zodId,
+ roleId: zodId,
+ matchType: z.string(),
+ opponentName: z.string(),
+ utc_ts: z.string().datetime(),
+ }),
+ timezone: z
+ .string()
+ .refine(
+ (value) => moment.tz.names().includes(value),
+ "Unknown timezone supplied",
+ ),
+});
diff --git a/src/routes/api/[guildId]/config.ts b/src/routes/api/[guildId]/config.ts
new file mode 100644
index 0000000..9eb7816
--- /dev/null
+++ b/src/routes/api/[guildId]/config.ts
@@ -0,0 +1,73 @@
+import { APIEvent } from "@solidjs/start/server/types";
+import { eq } from "drizzle-orm";
+import db from "~/drizzle";
+import { guilds } from "~/drizzle/schema";
+import { BasicAuth } from "~/lib/auth";
+import { buildConfig } from "~/lib/responseBuilders";
+import { ErrorResponse, Res } from "~/lib/responses";
+import { zodId } from "~/lib/zod";
+import { APIResponse } from "~/types/backend";
+
+type Path = "/api/{guildId}/config";
+
+export const GET = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ try {
+ zodId.parse(event.params.guildId);
+ } catch (e) {
+ return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
+ }
+
+ const guildQuery = await db.query.guilds
+ .findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: { tpMessages: true, matches: true },
+ })
+ .execute();
+
+ if (!guildQuery) return ErrorResponse("NOT_FOUND");
+
+ return Res("OK", buildConfig(guildQuery));
+};
+
+export const DELETE = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ try {
+ zodId.parse(event.params.guildId);
+ } catch (e) {
+ return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
+ }
+
+ const guildQuery = await db.query.guilds
+ .findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: { tpMessages: true, matches: true },
+ })
+ .execute();
+
+ if (!guildQuery) return ErrorResponse("NOT_FOUND");
+
+ await db.delete(guilds).where(eq(guilds.id, event.params.guildId)).execute();
+
+ return Res("NO_CONTENT", null);
+};
diff --git a/src/routes/api/[guildId]/matches.ts b/src/routes/api/[guildId]/matches.ts
new file mode 100644
index 0000000..703654a
--- /dev/null
+++ b/src/routes/api/[guildId]/matches.ts
@@ -0,0 +1,93 @@
+import { APIEvent } from "@solidjs/start/server/types";
+import { eq } from "drizzle-orm";
+import db from "~/drizzle";
+import { guilds, matches } from "~/drizzle/schema";
+import { BasicAuth } from "~/lib/auth";
+import { buildMatches } from "~/lib/responseBuilders";
+import { ErrorResponse, Res } from "~/lib/responses";
+import { zodMatch } from "~/lib/zod";
+import { APIResponse, RequestBody } from "~/types/backend";
+
+type Path = "/api/{guildId}/matches";
+
+export const GET = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ const guild = await db.query.guilds
+ .findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: {
+ matches: true,
+ },
+ })
+ .execute();
+
+ console.log(event.params.guildId, guild);
+
+ if (!guild) return ErrorResponse("NOT_FOUND");
+
+ if (guild.matches.length < 1) return Res("NO_CONTENT", null);
+
+ return Res("OK", {
+ matches: buildMatches(guild.matches),
+ timezone: guild.timezone,
+ });
+};
+
+export const POST = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ const guild = await db.query.guilds
+ .findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: {
+ matches: true,
+ },
+ })
+ .execute();
+
+ console.log(event.params.guildId, guild);
+
+ if (!guild) return ErrorResponse("NOT_FOUND");
+
+ const unparsedBody = await new Response(event.request.body).json();
+
+ let body: RequestBody;
+ try {
+ body = zodMatch.parse(unparsedBody);
+ } catch (e) {
+ return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
+ }
+
+ if (body.timezone !== guild.timezone)
+ return ErrorResponse(
+ "BAD_REQUEST",
+ "Match's timezone is different from guild's timezone",
+ );
+
+ await db.insert(matches).values({
+ ...body.match,
+ guildId: guild.id,
+ utc_ts: new Date(body.match.utc_ts),
+ });
+
+ return Res("NO_CONTENT", null);
+};
diff --git a/src/routes/api/[guildId]/tp_messages.ts b/src/routes/api/[guildId]/tp_messages.ts
new file mode 100644
index 0000000..c4cb99a
--- /dev/null
+++ b/src/routes/api/[guildId]/tp_messages.ts
@@ -0,0 +1,114 @@
+import { APIEvent } from "@solidjs/start/server/types";
+import { and, eq } from "drizzle-orm";
+import db from "~/drizzle";
+import { guilds, tpMessages } from "~/drizzle/schema";
+import { BasicAuth } from "~/lib/auth";
+import { ErrorResponse, Res } from "~/lib/responses";
+import { zodTpMessages } from "~/lib/zod";
+import { APIResponse, RequestBody } from "~/types/backend";
+
+type Path = "/api/{guildId}/tp_messages";
+
+const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
+type DayKeys = (typeof DayKeys)[number];
+type Messages = Record;
+
+export const GET = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ const guild = await db.query.guilds.findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: {
+ tpMessages: true,
+ },
+ });
+
+ if (!guild) return ErrorResponse("NOT_FOUND");
+
+ if (!guild.tpEnabled || !guild.tpChannelId) return Res("NO_CONTENT", null);
+
+ const tpMessages = guild.tpMessages.reduce(
+ (acc, message) => {
+ const day = message.day.toString() as DayKeys;
+ if (!/^[0-6]$/.test(day)) return acc;
+ acc[day] = message.messageId;
+ return acc;
+ },
+ {
+ "0": null,
+ "1": null,
+ "2": null,
+ "3": null,
+ "4": null,
+ "5": null,
+ "6": null,
+ } as Messages,
+ );
+
+ return Res("OK", {
+ channelId: guild.tpChannelId,
+ messageIds: tpMessages,
+ });
+};
+
+export const PUT = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ const guild = await db.query.guilds
+ .findFirst({
+ where: eq(guilds.id, event.params.guildId),
+ with: { tpMessages: true },
+ })
+ .execute();
+
+ if (!guild) return ErrorResponse("NOT_FOUND");
+
+ if (!guild.tpEnabled) return ErrorResponse("FORBIDDEN");
+
+ const unparsedBody = await new Response(event.request.body).json();
+
+ let body: RequestBody;
+ try {
+ body = zodTpMessages.parse(unparsedBody);
+ } catch (e) {
+ return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
+ }
+
+ if (guild.tpChannelId !== body.channelId)
+ await db
+ .update(guilds)
+ .set({ tpChannelId: body.channelId })
+ .where(eq(guilds.id, guild.id))
+ .execute();
+
+ await Promise.all(
+ DayKeys.map(async (dayStr) => {
+ const day = parseInt(dayStr);
+ await db
+ .update(tpMessages)
+ .set({ messageId: body.messageIds[dayStr] })
+ .where(and(eq(tpMessages.guildId, guild.id), eq(tpMessages.day, day)))
+ .execute();
+ }),
+ );
+
+ return Res("NO_CONTENT", null);
+};
diff --git a/src/routes/api/auth/callback/discord.ts b/src/routes/api/auth/callback/discord.ts
index 4ed93d0..c6f2ec0 100644
--- a/src/routes/api/auth/callback/discord.ts
+++ b/src/routes/api/auth/callback/discord.ts
@@ -2,6 +2,7 @@ import { createId } from "@paralleldrive/cuid2";
import { APIEvent } from "@solidjs/start/server/types";
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";
@@ -20,20 +21,20 @@ export async function GET(event: APIEvent): Promise {
switch (error) {
case "access_denied":
return new Response(null, {
- status: 302,
+ status: httpStatus.FOUND,
headers: { Location: "/" },
});
default:
console.log("Discord oauth error:", error_description);
return new Response(decodeURI(error_description ?? ""), {
- status: 400,
+ status: httpStatus.BAD_REQUEST,
});
}
const storedState = getCookie("discord_oauth_state") ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
- status: 400,
+ status: httpStatus.BAD_REQUEST,
});
}
@@ -77,7 +78,7 @@ export async function GET(event: APIEvent): Promise {
.execute();
return new Response(null, {
- status: 302,
+ status: httpStatus.FOUND,
headers: { Location: "/config" },
});
}
@@ -112,7 +113,7 @@ export async function GET(event: APIEvent): Promise {
sessionCookie.attributes,
);
return new Response(null, {
- status: 302,
+ status: httpStatus.FOUND,
headers: { Location: "/config" },
});
} catch (e) {
@@ -120,13 +121,13 @@ export async function GET(event: APIEvent): Promise {
if (e instanceof OAuth2RequestError) {
// invalid code
return new Response(null, {
- status: 400,
+ status: httpStatus.BAD_REQUEST,
});
}
console.error("Unknown error on callback.");
console.error(e);
return new Response(null, {
- status: 500,
+ status: httpStatus.INTERNAL_SERVER_ERROR,
});
}
}
diff --git a/src/routes/api/auth/login.ts b/src/routes/api/auth/login.ts
index b971d5b..1ae41b5 100644
--- a/src/routes/api/auth/login.ts
+++ b/src/routes/api/auth/login.ts
@@ -1,5 +1,6 @@
import { APIEvent } from "@solidjs/start/server/types";
import { generateState } from "arctic";
+import httpStatus from "http-status";
import { setCookie } from "vinxi/http";
import { discord } from "~/lib/auth";
@@ -18,7 +19,7 @@ export async function GET(event: APIEvent) {
});
return new Response(null, {
- status: 302,
+ status: httpStatus.FOUND,
headers: { Location: url.toString() },
});
}
diff --git a/src/routes/api/auth/logout.ts b/src/routes/api/auth/logout.ts
index 9222960..ece785d 100644
--- a/src/routes/api/auth/logout.ts
+++ b/src/routes/api/auth/logout.ts
@@ -1,4 +1,5 @@
import { APIEvent } from "@solidjs/start/server/types";
+import httpStatus from "http-status";
import { appendHeader } from "vinxi/http";
import { lucia } from "~/lib/auth";
@@ -13,7 +14,7 @@ export const GET = async (event: APIEvent) => {
lucia.createBlankSessionCookie().serialize(),
);
return new Response(null, {
- status: 302,
+ status: httpStatus.FOUND,
headers: { Location: "/" },
});
};
diff --git a/src/routes/api/boot.ts b/src/routes/api/boot.ts
new file mode 100644
index 0000000..bad071c
--- /dev/null
+++ b/src/routes/api/boot.ts
@@ -0,0 +1,35 @@
+import { APIEvent } from "@solidjs/start/server/types";
+import { eq } from "drizzle-orm";
+import db from "~/drizzle";
+import { guilds } from "~/drizzle/schema";
+import { BasicAuth } from "~/lib/auth";
+import { buildConfig } from "~/lib/responseBuilders";
+import { ErrorResponse, Res } from "~/lib/responses";
+import { APIResponse } from "~/types/backend";
+
+type Path = "/api/boot";
+
+export const GET = async (
+ event: APIEvent,
+): Promise> => {
+ switch (event.request.headers.get("authorization")) {
+ case BasicAuth.unencoded:
+ case BasicAuth.encoded:
+ break;
+
+ default:
+ return ErrorResponse("UNAUTHORIZED");
+ }
+
+ const guildQuery = await db.query.guilds
+ .findMany({
+ where: eq(guilds.id, event.params.guildId),
+ with: { tpMessages: true, matches: true },
+ })
+ .execute();
+
+ return Res(
+ "OK",
+ guildQuery.map((e) => buildConfig(e)),
+ );
+};
diff --git a/src/routes/api/bot/[guildId]/config.ts b/src/routes/api/bot/[guildId]/config.ts
deleted file mode 100644
index 00595dc..0000000
--- a/src/routes/api/bot/[guildId]/config.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { APIEvent } from "@solidjs/start/server/types";
-import { eq } from "drizzle-orm";
-import db from "~/drizzle";
-import { guilds } from "~/drizzle/schema";
-
-export const GET = async ({ params }: APIEvent) => {
- if (params.guildId === "boot") {
- const guilds = await db.query.guilds
- .findMany({
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- return { guilds };
- }
-
- const guild = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guild)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- return guild;
-};
-
-export const DELETE = async ({ params }: APIEvent) => {
- const guildQuery = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guildQuery)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- const guild = await db
- .delete(guilds)
- .where(eq(guilds.id, params.guildId))
- .returning()
- .execute();
-
- return guild;
-};
diff --git a/src/routes/api/bot/[guildId]/matches/[channelId]/[matchMessageId].ts b/src/routes/api/bot/[guildId]/matches/[channelId]/[matchMessageId].ts
deleted file mode 100644
index c701f81..0000000
--- a/src/routes/api/bot/[guildId]/matches/[channelId]/[matchMessageId].ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { APIEvent } from "@solidjs/start/server/types";
-import { eq } from "drizzle-orm";
-import db from "~/drizzle";
-import { guilds } from "~/drizzle/schema";
-
-export const PUT = async ({ params }: APIEvent) => {
- const guild = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guild)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- return "TODO";
- // return guild.timePlanning;
-};
diff --git a/src/routes/api/bot/[guildId]/matches/[channelId]/index.ts b/src/routes/api/bot/[guildId]/matches/[channelId]/index.ts
deleted file mode 100644
index bf84d60..0000000
--- a/src/routes/api/bot/[guildId]/matches/[channelId]/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { APIEvent } from "@solidjs/start/server/types";
-import { eq } from "drizzle-orm";
-import db from "~/drizzle";
-import { guilds } from "~/drizzle/schema";
-
-export const POST = async ({ params }: APIEvent) => {
- const guild = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guild)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- return "TODO";
- // return guild.timePlanning;
-};
diff --git a/src/routes/api/bot/[guildId]/matches/index.ts b/src/routes/api/bot/[guildId]/matches/index.ts
deleted file mode 100644
index 403aab3..0000000
--- a/src/routes/api/bot/[guildId]/matches/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { APIEvent } from "@solidjs/start/server/types";
-import { eq } from "drizzle-orm";
-import db from "~/drizzle";
-import { guilds } from "~/drizzle/schema";
-
-export const GET = async ({ params }: APIEvent) => {
- const guild = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guild)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- return "TODO";
- // return guild.timePlanning;
-};
diff --git a/src/routes/api/bot/[guildId]/tp_messages.ts b/src/routes/api/bot/[guildId]/tp_messages.ts
deleted file mode 100644
index 8ede1ed..0000000
--- a/src/routes/api/bot/[guildId]/tp_messages.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { APIEvent } from "@solidjs/start/server/types";
-import { eq } from "drizzle-orm";
-import db from "~/drizzle";
-import { guilds } from "~/drizzle/schema";
-
-export const GET = async ({ params }: APIEvent) => {
- const guild = await db.query.guilds
- .findFirst({
- where: eq(guilds.id, params.guildId),
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- if (!guild)
- return new Response(JSON.stringify({ error: "No such guild found." }), {
- status: 404,
- });
-
- return "TODO";
- // return guild.timePlanning;
-};
-
-export const PUT = async () => {
- return "TODO";
-};
diff --git a/src/routes/api/bot/boot/config.ts b/src/routes/api/bot/boot/config.ts
deleted file mode 100644
index c93810c..0000000
--- a/src/routes/api/bot/boot/config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import db from "~/drizzle";
-
-export const GET = async () => {
- const guilds = await db.query.guilds
- .findMany({
- with: {
- timePlanning: { with: { messages: true } },
- matches: true,
- },
- })
- .execute();
-
- return { guilds };
-};
diff --git a/src/types/authjs.d.ts b/src/types/authjs.d.ts
deleted file mode 100644
index 18d303c..0000000
--- a/src/types/authjs.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { DefaultSession as DSession } from "@auth/core/types"
-
-declare module "@auth/core/types" {
- /**
- * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
- */
- interface Session extends DSession {
- user?: {
- id: string
- name?: string | null
- email?: string | null
- image?: string | null
- }
- }
-}
diff --git a/src/types/backend.d.ts b/src/types/backend.d.ts
new file mode 100644
index 0000000..0d605b6
--- /dev/null
+++ b/src/types/backend.d.ts
@@ -0,0 +1,70 @@
+import { HttpStatus } from "http-status";
+import { paths } from "./liljudd";
+
+export type MyPaths = keyof paths;
+
+export type Methods = keyof paths[Path];
+
+export type Responses<
+ Path extends MyPaths,
+ Method extends Methods,
+> = "responses" extends keyof paths[Path][Method]
+ ? paths[Path][Method]["responses"]
+ : never;
+
+type StatusCodes> = {
+ [CodeName in keyof HttpStatus]: HttpStatus[CodeName] extends number
+ ? HttpStatus[CodeName] extends keyof Responses
+ ? CodeName
+ : never
+ : never;
+}[keyof HttpStatus];
+
+export type ResponseSchemas<
+ Path extends MyPaths,
+ Method extends Methods,
+ Code extends StatusCodes,
+> = Code extends keyof HttpStatus
+ ? HttpStatus[Code] extends keyof Responses
+ ? "content" extends keyof Responses[HttpStatus[Code]]
+ ? "application/json" extends keyof Responses<
+ Path,
+ Method
+ >[HttpStatus[Code]]["content"]
+ ? Responses<
+ Path,
+ Method
+ >[HttpStatus[Code]]["content"]["application/json"] extends never
+ ? null
+ : Responses<
+ Path,
+ Method
+ >[HttpStatus[Code]]["content"]["application/json"]
+ : never
+ : never
+ : never
+ : never;
+
+export type Parameters<
+ Path extends MyPaths,
+ Method extends Methods,
+> = "parameters" extends keyof paths[Path][Method]
+ ? "path" extends keyof paths[Path][Method]["parameters"]
+ ? paths[Path][Method]["parameters"]["path"]
+ : never
+ : never;
+
+export type RequestBody<
+ Path extends MyPaths,
+ Method extends Methods,
+> = "requestBody" extends keyof paths[Path][Method]
+ ? "content" extends keyof paths[Path][Method]["requestBody"]
+ ? "application/json" extends keyof paths[Path][Method]["requestBody"]["content"]
+ ? paths[Path][Method]["requestBody"]["content"]["application/json"]
+ : never
+ : never
+ : never;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export interface APIResponse>
+ extends Response {}
diff --git a/src/global.d.ts b/src/types/global.d.ts
similarity index 100%
rename from src/global.d.ts
rename to src/types/global.d.ts
diff --git a/src/types/liljudd.d.ts b/src/types/liljudd.d.ts
index 97f3c89..236c849 100644
--- a/src/types/liljudd.d.ts
+++ b/src/types/liljudd.d.ts
@@ -5,24 +5,48 @@
export interface paths {
- "/api/config/{guildId}": {
+ "/api/boot": {
/**
- * Find guild config by ID
- * @description Returns a single guild config
+ * Retrieve all guild's configs
+ * @description Returns all guild's configs.
+ */
+ get: operations["getGuildsForBoot"];
+ };
+ "/api/{guildId}/config": {
+ /**
+ * Find a guild's config by ID
+ * @description Returns a single guild's config.
*/
get: operations["getGuildById"];
/**
- * Deletes a guild config by ID
- * @description Delete a guild's config
+ * Deletes a guild's config by ID
+ * @description Delete a guild's config when the bot is removed from the guild.
*/
delete: operations["deleteGuildById"];
};
- "/api/tp_messages/{guildId}": {
+ "/api/{guildId}/tp_messages": {
/**
- * Find guild by ID for it's tp_messages
+ * Find the tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
get: operations["getTp_messagesOfGuildById"];
+ /**
+ * Put new message IDs for tp_messages of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ put: operations["putTp_messagesOfGuildById"];
+ };
+ "/api/{guildId}/matches": {
+ /**
+ * Find all matches of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ get: operations["getMatchesOfGuildById"];
+ /**
+ * Save a new created match of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ post: operations["postMatchOfGuildById"];
};
}
@@ -32,66 +56,124 @@ export interface components {
schemas: {
guildConfig: {
/**
- * Format: varchar(19)
+ * Format: varchar(20)
* @example 1234567890123456789
*/
- guildID?: string;
- features?: {
- time_planning?: {
+ guildId: string;
+ /**
+ * Format: text
+ * @example Europe/Berlin
+ */
+ timezone: string;
+ features: {
+ timePlanning: {
+ enabled: boolean;
/**
- * Format: varchar(19)
+ * Format: varchar(20)
* @example 1234567890123456789
*/
- channelID?: string;
- /** @example 0 0 1 * * * 60o 1w */
- cron?: string;
- /**
- * Format: varchar(19)
- * @example 1234567890123456789
- */
- isAvailableRoleId?: string;
- /**
- * Format: varchar(19)
- * @example 1234567890123456789
- */
- wantsToBeNotifieRoledId?: string;
+ channelId: string | null;
+ /** @example 0 */
+ targetMinute: number;
+ /** @example 1 */
+ targetHour: number;
+ /** @example 1 */
+ targetDay: number;
+ roles: {
+ enabled: boolean;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ isAvailableRoleId: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ wantsToBeNotifieRoledId: string | null;
+ };
};
};
- matches?: components["schemas"]["match"][];
+ matches: components["schemas"]["match"][];
+ checksum: string;
};
match: {
/**
- * Format: varchar(19)
+ * Format: varcharq(20)
* @example 1234567890123456789
*/
- channelID?: string;
+ channelId: string;
/**
* Format: varchar(50)
* @example Scrim
*/
- matchType?: string;
+ matchType: string;
/**
- * Format: varchar(19)
+ * Format: varchar(20)
* @example 1234567890123456789
*/
- createrId?: string;
+ createrId: string;
/**
- * Format: varchar(19)
+ * Format: varchar(20)
* @example 1234567890123456789
*/
- roleId?: string;
+ roleId: string;
/**
* Format: varchar(100)
* @example ?
*/
- opponentName?: string;
+ opponentName: string;
/**
- * Format: varchar(19)
+ * Format: varchar(20)
* @example 1234567890123456789
*/
- messsageId?: string;
- /** @example 0 0 1 5 2 2023 60o */
- cron?: string;
+ messageId: string;
+ /** @example 2020-01-01T00:00:00Z */
+ utc_ts: string;
+ };
+ tp_messages: {
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ channelId: string;
+ messageIds: {
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 0: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 1: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 2: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 3: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 4: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 5: string | null;
+ /**
+ * Format: varchar(20)
+ * @example 1234567890123456789
+ */
+ 6: string | null;
+ };
};
};
responses: never;
@@ -108,8 +190,34 @@ export type external = Record;
export interface operations {
/**
- * Find guild config by ID
- * @description Returns a single guild config
+ * Retrieve all guild's configs
+ * @description Returns all guild's configs.
+ */
+ getGuildsForBoot: {
+ responses: {
+ /** @description successful operation */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["guildConfig"][];
+ };
+ };
+ /** @description Invalid ID supplied */
+ 400: {
+ content: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
+ /** @description Guild not found */
+ 404: {
+ content: never;
+ };
+ };
+ };
+ /**
+ * Find a guild's config by ID
+ * @description Returns a single guild's config.
*/
getGuildById: {
parameters: {
@@ -129,6 +237,10 @@ export interface operations {
400: {
content: never;
};
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
/** @description Guild not found */
404: {
content: never;
@@ -136,8 +248,8 @@ export interface operations {
};
};
/**
- * Deletes a guild config by ID
- * @description Delete a guild's config
+ * Deletes a guild's config by ID
+ * @description Delete a guild's config when the bot is removed from the guild.
*/
deleteGuildById: {
parameters: {
@@ -155,6 +267,10 @@ export interface operations {
400: {
content: never;
};
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
/** @description Guild not found */
404: {
content: never;
@@ -162,7 +278,7 @@ export interface operations {
};
};
/**
- * Find guild by ID for it's tp_messages
+ * Find the tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
getTp_messagesOfGuildById: {
@@ -176,7 +292,7 @@ export interface operations {
/** @description successful operation */
200: {
content: {
- "application/json": components["schemas"]["guildConfig"];
+ "application/json": components["schemas"]["tp_messages"];
};
};
/** @description Time planning not enabled for this guild */
@@ -187,6 +303,137 @@ export interface operations {
400: {
content: never;
};
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
+ /** @description Guild not found */
+ 404: {
+ content: never;
+ };
+ };
+ };
+ /**
+ * Put new message IDs for tp_messages of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ putTp_messagesOfGuildById: {
+ parameters: {
+ path: {
+ /** @description ID of guild's tp_messages to return */
+ guildId: string;
+ };
+ };
+ /** @description Put new message IDs for tp_messages in channel */
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["tp_messages"];
+ };
+ };
+ responses: {
+ /** @description successful operation */
+ 204: {
+ content: never;
+ };
+ /** @description Invalid ID supplied */
+ 400: {
+ content: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
+ /** @description Time planning not enabled for this guild */
+ 403: {
+ content: never;
+ };
+ /** @description Guild not found */
+ 404: {
+ content: never;
+ };
+ };
+ };
+ /**
+ * Find all matches of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ getMatchesOfGuildById: {
+ parameters: {
+ path: {
+ /** @description ID of guild's tp_messages to return */
+ guildId: string;
+ };
+ };
+ responses: {
+ /** @description successful operation */
+ 200: {
+ content: {
+ "application/json": {
+ matches: components["schemas"]["match"][];
+ /**
+ * Format: text
+ * @example Europe/Berlin
+ */
+ timezone: string;
+ };
+ };
+ };
+ /** @description Time planning not enabled for this guild */
+ 204: {
+ content: never;
+ };
+ /** @description Invalid ID supplied */
+ 400: {
+ content: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
+ /** @description Guild not found */
+ 404: {
+ content: never;
+ };
+ };
+ };
+ /**
+ * Save a new created match of guild by ID
+ * @description Returns tp_messages for a guild
+ */
+ postMatchOfGuildById: {
+ parameters: {
+ path: {
+ /** @description ID of match's guild to set */
+ guildId: string;
+ };
+ };
+ /** @description Save a new created match in channel */
+ requestBody: {
+ content: {
+ "application/json": {
+ match: components["schemas"]["match"];
+ /**
+ * Format: text
+ * @description Has to match guild tz
+ * @example Europe/Berlin
+ */
+ timezone: string;
+ };
+ };
+ };
+ responses: {
+ /** @description successful operation */
+ 204: {
+ content: never;
+ };
+ /** @description Invalid ID supplied */
+ 400: {
+ content: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ content: never;
+ };
/** @description Guild not found */
404: {
content: never;
diff --git a/src/types/lucia-auth.d.ts b/src/types/lucia-auth.d.ts
new file mode 100644
index 0000000..aea0005
--- /dev/null
+++ b/src/types/lucia-auth.d.ts
@@ -0,0 +1,25 @@
+import { PgColumn, PgTableWithColumns } from "drizzle-orm/pg-core";
+import { users } from "~/drizzle/schema";
+import { lucia } from "~/lib/auth";
+
+declare module "lucia" {
+ // eslint-disable-next-line no-unused-vars
+ interface Register {
+ Lucia: typeof lucia;
+ DatabaseUserAttributes: DatabaseUserAttributes;
+ }
+}
+
+type GetColumns =
+ T extends PgTableWithColumns ? First["columns"] : never;
+
+type ExtractDataTypes = {
+ [K in keyof T]: T[K] extends PgColumn
+ ? DataType["data"]
+ : never;
+};
+
+interface DatabaseUserAttributes
+ extends ExtractDataTypes> {
+ warst: string;
+}