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"; import { generationQueue } from "../app/generationQueue.ts";
export const jobsRoute = { export const jobsRoute = createMethodFilter({
GET: new Endpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async () => ({ async () => ({
status: 200, status: 200,
type: "application/json", body: {
body: await generationQueue.getAllJobs(), type: "application/json",
data: await generationQueue.getAllJobs(),
},
}), }),
), ),
} satisfies Route; });

View File

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

View File

@ -1,26 +1,22 @@
import { deepMerge } from "std/collections/deep_merge.ts"; import { deepMerge } from "std/collections/deep_merge.ts";
import { getLogger } from "std/log/mod.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 { configSchema, getConfig, setConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts"; import { bot } from "../bot/mod.ts";
import { sessions } from "./sessionsRoute.ts"; import { sessions } from "./sessionsRoute.ts";
export const logger = () => getLogger(); export const logger = () => getLogger();
export const paramsRoute = { export const paramsRoute = createMethodFilter({
GET: new Endpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async () => { async () => {
const config = await getConfig(); const config = await getConfig();
return { return { status: 200, body: { type: "application/json", data: config.defaultParams } };
status: 200,
type: "application/json",
body: config?.defaultParams,
};
}, },
), ),
PATCH: new Endpoint( PATCH: createEndpoint(
{ {
query: { sessionId: { type: "string" } }, query: { sessionId: { type: "string" } },
body: { body: {
@ -31,7 +27,7 @@ export const paramsRoute = {
async ({ query, body }) => { async ({ query, body }) => {
const session = sessions.get(query.sessionId); const session = sessions.get(query.sessionId);
if (!session?.userId) { 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); const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") { if (chat.type !== "private") {
@ -39,16 +35,16 @@ export const paramsRoute = {
} }
const userName = chat.username; const userName = chat.username;
if (!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(); const config = await getConfig();
if (!config?.adminUsernames?.includes(userName)) { 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)}`); logger().info(`User ${userName} updated default params: ${JSON.stringify(body.data)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body); const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
await setConfig({ defaultParams }); 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 { jobsRoute } from "./jobsRoute.ts";
import { sessionsRoute } from "./sessionsRoute.ts"; import { sessionsRoute } from "./sessionsRoute.ts";
import { usersRoute } from "./usersRoute.ts"; import { usersRoute } from "./usersRoute.ts";
import { paramsRoute } from "./paramsRoute.ts"; import { paramsRoute } from "./paramsRoute.ts";
export const api = new Api({ export const serveApi = createPathFilter({
"jobs": jobsRoute, "jobs": jobsRoute,
"sessions": sessionsRoute, "sessions": sessionsRoute,
"users": usersRoute, "users": usersRoute,
"settings/params": paramsRoute, "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 // 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"; import { ulid } from "ulid";
export const sessions = new Map<string, Session>(); export const sessions = new Map<string, Session>();
@ -8,25 +8,30 @@ export interface Session {
userId?: number; userId?: number;
} }
export const sessionsRoute = { export const sessionsRoute = createPathFilter({
POST: new Endpoint( "": createMethodFilter({
{ query: null, body: null }, POST: createEndpoint(
async () => { { query: null, body: null },
const id = ulid(); async () => {
const session: Session = {}; const id = ulid();
sessions.set(id, session); const session: Session = {};
return { status: 200, type: "application/json", body: { id, ...session } }; sessions.set(id, session);
}, return { status: 200, body: { type: "application/json", data: { id, ...session } } };
), },
GET: new Endpoint( ),
{ query: { sessionId: { type: "string" } }, body: null }, }),
async ({ query }) => {
const id = query.sessionId; "{sessionId}": createMethodFilter({
const session = sessions.get(id); GET: createEndpoint(
if (!session) { { query: null, body: null },
return { status: 401, type: "text/plain", body: "Session not found" }; async ({ params }) => {
} const id = params.sessionId;
return { status: 200, type: "application/json", body: { id, ...session } }; const session = sessions.get(id);
}, if (!session) {
), return { status: 401, body: { type: "text/plain", data: "Session not found" } };
} satisfies Route; }
return { status: 200, body: { type: "application/json", data: { id, ...session } } };
},
),
}),
});

View File

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

View File

@ -1,6 +1,7 @@
{ {
"tasks": { "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": { "compilerOptions": {
"jsx": "react" "jsx": "react"
@ -33,9 +34,9 @@
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0", "png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
"png_chunk_text": "https://esm.sh/png-chunk-text@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", "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": "https://esm.sh/react@18.2.0?dev",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?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", "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 { AppHeader } from "./AppHeader.tsx";
import { QueuePage } from "./QueuePage.tsx"; import { QueuePage } from "./QueuePage.tsx";
import { SettingsPage } from "./SettingsPage.tsx"; import { SettingsPage } from "./SettingsPage.tsx";
import { apiClient, handleResponse } from "./apiClient.tsx"; import { fetchApi, handleResponse } from "./apiClient.tsx";
export function App() { export function App() {
// store session ID in the local storage // store session ID in the local storage
@ -13,7 +13,7 @@ export function App() {
// initialize a new session when there is no session ID // initialize a new session when there is no session ID
useEffect(() => { useEffect(() => {
if (!sessionId) { if (!sessionId) {
apiClient.fetch("sessions", "POST", {}).then(handleResponse).then((session) => { fetchApi("sessions", "POST", {}).then(handleResponse).then((session) => {
console.log("Initialized session", session.id); console.log("Initialized session", session.id);
setSessionId(session.id); setSessionId(session.id);
}); });

View File

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

View File

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

View File

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

View File

@ -1,12 +1,15 @@
import { Client } from "t_rest/client"; import { createFetcher, Output } from "t_rest/client";
import { ApiResponse } from "t_rest/server"; import { ApiHandler } from "../api/serveApi.ts";
import { ErisApi } from "../api/api.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) { if (response.status !== 200) {
throw new Error(String(response.body)); throw new Error(String(response.body.data));
} }
return response.body; return response.body.data;
} }