update rest api

This commit is contained in:
pinks 2023-10-08 23:23:54 +02:00
parent c0354ef679
commit a8d36db20b
12 changed files with 116 additions and 110 deletions

View File

@ -1,13 +1,15 @@
import { Endpoint, Route } from "t_rest/server";
import { createEndpoint, createMethodFilter } from "t_rest/server";
import { generationQueue } from "../app/generationQueue.ts";
export const jobsRoute = {
GET: new Endpoint(
export const jobsRoute = createMethodFilter({
GET: createEndpoint(
{ query: null, body: null },
async () => ({
status: 200,
type: "application/json",
body: await generationQueue.getAllJobs(),
body: {
type: "application/json",
data: await generationQueue.getAllJobs(),
},
}),
),
} satisfies Route;
});

View File

@ -1,11 +1,11 @@
import { route } from "reroute";
import { serveSpa } from "serve_spa";
import { api } from "./api.ts";
import { serveApi } from "./serveApi.ts";
export async function serveUi() {
const server = Deno.serve({ port: 5999 }, (request) =>
route(request, {
"/api/*": (request) => api.serve(request),
"/api/*": (request) => serveApi(request),
"/*": (request) =>
serveSpa(request, {
fsRoot: new URL("../ui/", import.meta.url).pathname,

View File

@ -1,26 +1,22 @@
import { deepMerge } from "std/collections/deep_merge.ts";
import { getLogger } from "std/log/mod.ts";
import { Endpoint, Route } from "t_rest/server";
import { createEndpoint, createMethodFilter } from "t_rest/server";
import { configSchema, getConfig, setConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts";
import { sessions } from "./sessionsRoute.ts";
export const logger = () => getLogger();
export const paramsRoute = {
GET: new Endpoint(
export const paramsRoute = createMethodFilter({
GET: createEndpoint(
{ query: null, body: null },
async () => {
const config = await getConfig();
return {
status: 200,
type: "application/json",
body: config?.defaultParams,
};
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
},
),
PATCH: new Endpoint(
PATCH: createEndpoint(
{
query: { sessionId: { type: "string" } },
body: {
@ -31,7 +27,7 @@ export const paramsRoute = {
async ({ query, body }) => {
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, type: "text/plain", body: "Must be logged in" };
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") {
@ -39,16 +35,16 @@ export const paramsRoute = {
}
const userName = chat.username;
if (!userName) {
return { status: 403, type: "text/plain", body: "Must have a username" };
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(userName)) {
return { status: 403, type: "text/plain", body: "Must be an admin" };
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
logger().info(`User ${userName} updated default params: ${JSON.stringify(body)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body);
logger().info(`User ${userName} updated default params: ${JSON.stringify(body.data)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
await setConfig({ defaultParams });
return { status: 200, type: "application/json", body: config.defaultParams };
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
},
),
} satisfies Route;
});

View File

@ -1,14 +1,14 @@
import { Api } from "t_rest/server";
import { createPathFilter } from "t_rest/server";
import { jobsRoute } from "./jobsRoute.ts";
import { sessionsRoute } from "./sessionsRoute.ts";
import { usersRoute } from "./usersRoute.ts";
import { paramsRoute } from "./paramsRoute.ts";
export const api = new Api({
export const serveApi = createPathFilter({
"jobs": jobsRoute,
"sessions": sessionsRoute,
"users": usersRoute,
"settings/params": paramsRoute,
});
export type ErisApi = typeof api;
export type ApiHandler = typeof serveApi;

View File

@ -1,5 +1,5 @@
// deno-lint-ignore-file require-await
import { Endpoint, Route } from "t_rest/server";
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { ulid } from "ulid";
export const sessions = new Map<string, Session>();
@ -8,25 +8,30 @@ export interface Session {
userId?: number;
}
export const sessionsRoute = {
POST: new Endpoint(
{ query: null, body: null },
async () => {
const id = ulid();
const session: Session = {};
sessions.set(id, session);
return { status: 200, type: "application/json", body: { id, ...session } };
},
),
GET: new Endpoint(
{ query: { sessionId: { type: "string" } }, body: null },
async ({ query }) => {
const id = query.sessionId;
const session = sessions.get(id);
if (!session) {
return { status: 401, type: "text/plain", body: "Session not found" };
}
return { status: 200, type: "application/json", body: { id, ...session } };
},
),
} satisfies Route;
export const sessionsRoute = createPathFilter({
"": createMethodFilter({
POST: createEndpoint(
{ query: null, body: null },
async () => {
const id = ulid();
const session: Session = {};
sessions.set(id, session);
return { status: 200, body: { type: "application/json", data: { id, ...session } } };
},
),
}),
"{sessionId}": createMethodFilter({
GET: createEndpoint(
{ query: null, body: null },
async ({ params }) => {
const id = params.sessionId;
const session = sessions.get(id);
if (!session) {
return { status: 401, body: { type: "text/plain", data: "Session not found" } };
}
return { status: 200, body: { type: "application/json", data: { id, ...session } } };
},
),
}),
});

View File

@ -1,36 +1,36 @@
import { encode } from "std/encoding/base64.ts";
import { Endpoint, Route } from "t_rest/server";
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { getConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts";
export const usersRoute = {
GET: new Endpoint(
{ query: { userId: { type: "number" } }, body: null },
async ({ query }) => {
const chat = await bot.api.getChat(query.userId);
if (chat.type !== "private") {
throw new Error("Chat is not private");
}
const photoData = chat.photo?.small_file_id
? encode(
await fetch(
`https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile(
chat.photo.small_file_id,
).then((file) => file.file_path)}`,
).then((resp) => resp.arrayBuffer()),
)
: undefined;
const config = await getConfig();
const isAdmin = config?.adminUsernames?.includes(chat.username);
return {
status: 200,
type: "application/json",
body: {
...chat,
photoData,
isAdmin,
},
};
},
),
} satisfies Route;
export const usersRoute = createPathFilter({
"{userId}": createMethodFilter({
GET: createEndpoint(
{ query: null, body: null },
async ({ params }) => {
const chat = await bot.api.getChat(params.userId);
if (chat.type !== "private") {
throw new Error("Chat is not private");
}
const photoData = chat.photo?.small_file_id
? encode(
await fetch(
`https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile(
chat.photo.small_file_id,
).then((file) => file.file_path)}`,
).then((resp) => resp.arrayBuffer()),
)
: undefined;
const config = await getConfig();
const isAdmin = config?.adminUsernames?.includes(chat.username);
return {
status: 200,
body: {
type: "application/json",
data: { ...chat, photoData, isAdmin },
},
};
},
),
}),
});

View File

@ -1,6 +1,7 @@
{
"tasks": {
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts"
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts",
"check": "deno check --unstable main.ts && deno check --unstable ui/main.tsx"
},
"compilerOptions": {
"jsx": "react"
@ -33,9 +34,9 @@
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
"png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0",
"openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6",
"t_rest/server": "https://esm.sh/ty-rest@0.2.1/server?dev",
"t_rest/server": "https://esm.sh/ty-rest@0.3.1/server?dev",
"t_rest/client": "https://esm.sh/ty-rest@0.2.1/client?dev",
"t_rest/client": "https://esm.sh/ty-rest@0.3.1/client?dev",
"react": "https://esm.sh/react@18.2.0?dev",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
"react-router-dom": "https://esm.sh/react-router-dom@6.16.0?dev",

View File

@ -4,7 +4,7 @@ import useLocalStorage from "use-local-storage";
import { AppHeader } from "./AppHeader.tsx";
import { QueuePage } from "./QueuePage.tsx";
import { SettingsPage } from "./SettingsPage.tsx";
import { apiClient, handleResponse } from "./apiClient.tsx";
import { fetchApi, handleResponse } from "./apiClient.tsx";
export function App() {
// store session ID in the local storage
@ -13,7 +13,7 @@ export function App() {
// initialize a new session when there is no session ID
useEffect(() => {
if (!sessionId) {
apiClient.fetch("sessions", "POST", {}).then(handleResponse).then((session) => {
fetchApi("sessions", "POST", {}).then(handleResponse).then((session) => {
console.log("Initialized session", session.id);
setSessionId(session.id);
});

View File

@ -2,7 +2,7 @@ import { cx } from "@twind/core";
import React, { ReactNode } from "react";
import { NavLink } from "react-router-dom";
import useSWR from "swr";
import { apiClient, handleResponse } from "./apiClient.tsx";
import { fetchApi, handleResponse } from "./apiClient.tsx";
function NavTab(props: { to: string; children: ReactNode }) {
return (
@ -21,16 +21,16 @@ export function AppHeader(
const { className, sessionId, onLogOut } = props;
const session = useSWR(
sessionId ? ["sessions", "GET", { query: { sessionId } }] as const : null,
(args) => apiClient.fetch(...args).then(handleResponse),
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
(args) => fetchApi(...args).then(handleResponse),
{ onError: () => onLogOut() },
);
const user = useSWR(
session.data?.userId
? ["users", "GET", { query: { userId: session.data.userId } }] as const
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
: null,
(args) => apiClient.fetch(...args).then(handleResponse),
(args) => fetchApi(...args).then(handleResponse),
);
return (

View File

@ -3,12 +3,12 @@ import FlipMove from "react-flip-move";
import useSWR from "swr";
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
import { Progress } from "./Progress.tsx";
import { apiClient, handleResponse } from "./apiClient.tsx";
import { fetchApi, handleResponse } from "./apiClient.tsx";
export function QueuePage() {
const jobs = useSWR(
["jobs", "GET", {}] as const,
(args) => apiClient.fetch(...args).then(handleResponse),
(args) => fetchApi(...args).then(handleResponse),
{ refreshInterval: 2000 },
);

View File

@ -1,23 +1,23 @@
import { cx } from "@twind/core";
import React, { ReactNode, useState } from "react";
import useSWR from "swr";
import { apiClient, handleResponse } from "./apiClient.tsx";
import { fetchApi, handleResponse } from "./apiClient.tsx";
export function SettingsPage(props: { sessionId: string }) {
const { sessionId } = props;
const session = useSWR(
["sessions", "GET", { query: { sessionId } }] as const,
(args) => apiClient.fetch(...args).then(handleResponse),
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
(args) => fetchApi(...args).then(handleResponse),
);
const user = useSWR(
session.data?.userId
? ["users", "GET", { query: { userId: session.data.userId } }] as const
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
: null,
(args) => apiClient.fetch(...args).then(handleResponse),
(args) => fetchApi(...args).then(handleResponse),
);
const params = useSWR(
["settings/params", "GET", {}] as const,
(args) => apiClient.fetch(...args).then(handleResponse),
(args) => fetchApi(...args).then(handleResponse),
);
const [changedParams, setChangedParams] = useState<Partial<typeof params.data>>({});
const [error, setError] = useState<string>();
@ -28,10 +28,9 @@ export function SettingsPage(props: { sessionId: string }) {
onSubmit={(e) => {
e.preventDefault();
params.mutate(() =>
apiClient.fetch("settings/params", "PATCH", {
fetchApi("settings/params", "PATCH", {
query: { sessionId },
type: "application/json",
body: changedParams ?? {},
body: { type: "application/json", data: changedParams ?? {} },
}).then(handleResponse)
)
.then(() => setChangedParams({}))

View File

@ -1,12 +1,15 @@
import { Client } from "t_rest/client";
import { ApiResponse } from "t_rest/server";
import { ErisApi } from "../api/api.ts";
import { createFetcher, Output } from "t_rest/client";
import { ApiHandler } from "../api/serveApi.ts";
export const apiClient = new Client<ErisApi>(`${location.origin}/api/`);
export const fetchApi = createFetcher<ApiHandler>({
baseUrl: `${location.origin}/api/`,
});
export function handleResponse<T extends ApiResponse>(response: T): (T & { status: 200 })["body"] {
export function handleResponse<T extends Output>(
response: T,
): (T & { status: 200 })["body"]["data"] {
if (response.status !== 200) {
throw new Error(String(response.body));
throw new Error(String(response.body.data));
}
return response.body;
return response.body.data;
}