feat: 1-to-1 message render + web data-lake backend
This commit is contained in:
1 parent
f0afb7ec5b
commit
75425d1bee
110 files changed
+10199
-54
No files matched your search
@@ -0,0 +1,141 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
import { auth } from "$lib/stores/auth.svelte";
|
||||
|
||||
const BASE = import.meta.env.VITE_API_BASE ?? "/api";
|
||||
const MAX_LIMIT = 500;
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
detail: string;
|
||||
|
||||
constructor(status: number, detail: string) {
|
||||
super(detail);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
type QueryValue = string | number | boolean | null | undefined;
|
||||
|
||||
interface RequestOptions {
|
||||
account?: boolean;
|
||||
body?: unknown;
|
||||
method?: string;
|
||||
query?: Record<string, QueryValue>;
|
||||
}
|
||||
|
||||
function buildQuery(
|
||||
query: Record<string, QueryValue> | undefined,
|
||||
withAccount: boolean
|
||||
): string {
|
||||
const params = new URLSearchParams();
|
||||
if (withAccount && accounts.selectedId !== null) {
|
||||
params.set("account_id", String(accounts.selectedId));
|
||||
}
|
||||
if (query) {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === null || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
const clamped =
|
||||
key === "limit" && typeof value === "number"
|
||||
? Math.min(value, MAX_LIMIT)
|
||||
: value;
|
||||
params.set(key, String(clamped));
|
||||
}
|
||||
}
|
||||
const text = params.toString();
|
||||
return text ? `?${text}` : "";
|
||||
}
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};
|
||||
}
|
||||
|
||||
async function handleError(response: Response): Promise<never> {
|
||||
if (response.status === 401) {
|
||||
auth.logout();
|
||||
await goto("/login");
|
||||
}
|
||||
let detail = response.statusText;
|
||||
try {
|
||||
const data = await response.json();
|
||||
if (data && typeof data.detail === "string") {
|
||||
detail = data.detail;
|
||||
}
|
||||
} catch {
|
||||
detail = response.statusText;
|
||||
}
|
||||
throw new ApiError(response.status, detail);
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
const { method = "GET", query, body, account = false } = options;
|
||||
const headers: Record<string, string> = authHeaders();
|
||||
if (body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
const response = await fetch(`${BASE}${path}${buildQuery(query, account)}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return handleError(response);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export type MediaResult =
|
||||
| { state: "ready"; url: string; mime: string | null }
|
||||
| { state: "not-downloaded" }
|
||||
| { state: "missing" };
|
||||
|
||||
export async function requestMedia(mediaId: number): Promise<MediaResult> {
|
||||
const response = await fetch(`${BASE}/media/${mediaId}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (response.status === 409) {
|
||||
return { state: "not-downloaded" };
|
||||
}
|
||||
if (response.status === 404) {
|
||||
return { state: "missing" };
|
||||
}
|
||||
if (!response.ok) {
|
||||
return handleError(response);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
return {
|
||||
state: "ready",
|
||||
url: URL.createObjectURL(blob),
|
||||
mime: response.headers.get("Content-Type"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestMediaVersion(
|
||||
versionId: number
|
||||
): Promise<MediaResult> {
|
||||
const response = await fetch(`${BASE}/media/version/${versionId}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
return { state: "missing" };
|
||||
}
|
||||
if (!response.ok) {
|
||||
return handleError(response);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
return {
|
||||
state: "ready",
|
||||
url: URL.createObjectURL(blob),
|
||||
mime: response.headers.get("Content-Type"),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user