you know what let's just change frontend framework again

This commit is contained in:
sam 2024-11-24 15:55:29 +01:00
parent c8cd483d20
commit 0d47f1fb01
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
115 changed files with 4407 additions and 10824 deletions

View file

@ -0,0 +1,54 @@
export default class ApiError {
raw?: RawApiError;
code: ErrorCode;
constructor(err?: RawApiError, code?: ErrorCode) {
this.raw = err;
this.code = err?.code || code || ErrorCode.InternalServerError;
}
get obj(): RawApiError {
return this.toObject();
}
toObject(): RawApiError {
return {
status: this.raw?.status || 500,
code: this.code,
message: this.raw?.message || "Internal server error",
errors: this.raw?.errors,
};
}
}
export type RawApiError = {
status: number;
message: string;
code: ErrorCode;
errors?: Array<{ key: string; errors: ValidationError[] }>;
};
export enum ErrorCode {
InternalServerError = "INTERNAL_SERVER_ERROR",
Forbidden = "FORBIDDEN",
BadRequest = "BAD_REQUEST",
AuthenticationError = "AUTHENTICATION_ERROR",
AuthenticationRequired = "AUTHENTICATION_REQUIRED",
MissingScopes = "MISSING_SCOPES",
GenericApiError = "GENERIC_API_ERROR",
UserNotFound = "USER_NOT_FOUND",
MemberNotFound = "MEMBER_NOT_FOUND",
AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED",
LastAuthMethod = "LAST_AUTH_METHOD",
// This code isn't actually returned by the API
Non204Response = "(non 204 response)",
}
export type ValidationError = {
message: string;
min_length?: number;
max_length?: number;
actual_length?: number;
allowed_values?: any[];
actual_value?: any;
};

View file

@ -0,0 +1,92 @@
import { PUBLIC_API_BASE } from "$env/static/public";
import type { Cookies } from "@sveltejs/kit";
import ApiError, { ErrorCode } from "./error";
import { TOKEN_COOKIE_NAME } from "$lib";
import log from "$lib/log";
export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export type RequestArgs = {
token?: string;
isInternal?: boolean;
body?: any;
fetch?: typeof fetch;
cookies?: Cookies;
};
/**
* Makes a raw request to the API.
* @param method The HTTP method for this request
* @param path The path for this request, without the /api/v2 prefix, starting with a slash.
* @param args Optional arguments to the request function.
* @returns A Promise object.
*/
export async function baseRequest(
method: Method,
path: string,
args: RequestArgs = {},
): Promise<Response> {
const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME);
const fetchFn = args.fetch ?? fetch;
const url = `${PUBLIC_API_BASE}/${args.isInternal ? "internal" : "v2"}${path}`;
log.debug("Sending request to %s %s", method, url);
const headers = {
...(args.body ? { "Content-Type": "application/json; charset=utf-8" } : {}),
...(token ? { Authorization: token } : {}),
};
return await fetchFn(url, {
method,
headers,
body: args.body ? JSON.stringify(args.body) : undefined,
});
}
/**
* Makes a request to the API and parses the returned object.
* @param method The HTTP method for this request
* @param path The path for this request, without the /api/v2 prefix, starting with a slash.
* @param args Optional arguments to the request function.
* @returns The response deserialized as `T`.
*/
export async function apiRequest<T>(
method: Method,
path: string,
args: RequestArgs = {},
): Promise<T> {
const resp = await baseRequest(method, path, args);
if (resp.status < 200 || resp.status > 299) {
const err = await resp.json();
if ("code" in err) throw new ApiError(err);
else throw new ApiError();
}
return (await resp.json()) as T;
}
/**
* Makes a request without reading the body (unless the API returns an error).
* @param method The HTTP method for this request
* @param path The path for this request, without the /api/v2 prefix, starting with a slash.
* @param args Optional arguments to the request function.
* @param enforce204 Whether to throw an error on a non-204 status code.
*/
export async function fastRequest(
method: Method,
path: string,
args: RequestArgs = {},
enforce204: boolean = false,
): Promise<void> {
const resp = await baseRequest(method, path, args);
if (resp.status < 200 || resp.status > 299) {
const err = await resp.json();
if ("code" in err) throw new ApiError(err);
else throw new ApiError();
}
if (enforce204 && resp.status !== 204) throw new ApiError(undefined, ErrorCode.Non204Response);
}

View file

@ -0,0 +1,23 @@
import type { User } from "./user";
export type AuthResponse = {
user: User;
token: string;
expires_at: string;
};
export type CallbackResponse = {
has_account: boolean;
ticket?: string;
remote_username?: string;
user?: User;
token?: string;
expires_at?: string;
};
export type AuthUrls = {
email_enabled: boolean;
discord?: string;
google?: string;
tumblr?: string;
};

View file

@ -0,0 +1,4 @@
export * from "./meta";
export * from "./user";
export * from "./member";
export * from "./auth";

View file

@ -0,0 +1,9 @@
import type { Field, PartialMember, PartialUser, PrideFlag } from "./user";
export type Member = PartialMember & {
fields: Field[];
flags: PrideFlag[];
links: string[];
user: PartialUser;
};

View file

@ -0,0 +1,19 @@
export type Meta = {
repository: string;
version: string;
hash: string;
users: {
total: number;
active_month: number;
active_week: number;
active_day: number;
};
members: number;
limits: Limits;
};
export type Limits = {
member_count: number;
bio_length: number;
custom_preferences: number;
};

View file

@ -0,0 +1,139 @@
export type PartialUser = {
id: string;
username: string;
display_name: string | null;
avatar_url: string | null;
custom_preferences: Record<string, CustomPreference>;
};
export type User = PartialUser & {
bio: string | null;
member_title: string | null;
links: string[];
names: FieldEntry[];
pronouns: Pronoun[];
fields: Field[];
flags: PrideFlag[];
role: "USER" | "MODERATOR" | "ADMIN";
};
export type MeUser = UserWithMembers & {
auth_methods: AuthMethod[];
member_list_hidden: boolean;
last_active: string;
last_sid_reroll: string;
};
export type UserWithMembers = User & { members: PartialMember[] };
export type UserWithHiddenFields = User & {
auth_methods?: unknown[];
member_list_hidden: boolean;
last_active: string;
};
export type UserSettings = {
dark_mode: boolean | null;
};
export type PartialMember = {
id: string;
name: string;
display_name: string;
bio: string | null;
avatar_url: string | null;
names: FieldEntry[];
pronouns: Pronoun[];
unlisted: boolean | null;
};
export type FieldEntry = {
value: string;
status: string;
};
export type Pronoun = FieldEntry & { display_text: string | null };
export type Field = {
name: string;
entries: FieldEntry[];
};
export type PrideFlag = {
id: string;
image_url: string;
name: string;
description: string | null;
};
export type AuthMethod = {
id: string;
type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL";
remote_id: string;
remote_username?: string;
};
export type CustomPreference = {
icon: string;
tooltip: string;
muted: boolean;
favourite: boolean;
size: PreferenceSize;
};
export enum PreferenceSize {
Large = "LARGE",
Normal = "NORMAL",
Small = "SMALL",
}
export function mergePreferences(
prefs: Record<string, CustomPreference>,
): Record<string, CustomPreference> {
return Object.assign({}, defaultPreferences, prefs);
}
export const defaultPreferences = Object.freeze({
favourite: {
icon: "heart-fill",
tooltip: "Favourite",
size: PreferenceSize.Large,
muted: false,
favourite: true,
},
okay: {
icon: "hand-thumbs-up",
tooltip: "Okay",
size: PreferenceSize.Normal,
muted: false,
favourite: false,
},
jokingly: {
icon: "emoji-laughing",
tooltip: "Jokingly",
size: PreferenceSize.Normal,
muted: false,
favourite: false,
},
friends_only: {
icon: "people",
tooltip: "Friends only",
size: PreferenceSize.Normal,
muted: false,
favourite: false,
},
avoid: {
icon: "hand-thumbs-down",
tooltip: "Avoid",
size: PreferenceSize.Small,
muted: true,
favourite: false,
},
missing: {
icon: "question-lg",
tooltip: "Unknown (missing)",
size: PreferenceSize.Normal,
muted: false,
favourite: false,
},
});