Compare commits

..

No commits in common. "2665fa1c022096513c969ea571a121563924097b" and "2f059ceaff5fb3fc068311aae8625def7bb18077" have entirely different histories.

9 changed files with 147 additions and 92 deletions

View File

@ -1,9 +0,0 @@
import { createEndpoint, createMethodFilter } from "t_rest/server";
import { bot } from "../bot/mod.ts";
export const botRoute = createMethodFilter({
GET: createEndpoint({ query: null, body: null }, async () => {
const username = bot.botInfo.username;
return { status: 200, body: { type: "application/json", data: { username } } };
}),
});

View File

@ -2,7 +2,8 @@ import { deepMerge } from "std/collections/deep_merge.ts";
import { info } from "std/log/mod.ts";
import { createEndpoint, createMethodFilter } from "t_rest/server";
import { configSchema, getConfig, setConfig } from "../app/config.ts";
import { withUser } from "./withUser.ts";
import { bot } from "../bot/mod.ts";
import { sessions } from "./sessionsRoute.ts";
export const paramsRoute = createMethodFilter({
GET: createEndpoint(
@ -22,13 +23,23 @@ export const paramsRoute = createMethodFilter({
},
},
async ({ query, body }) => {
return withUser(query, async (chat) => {
const config = await getConfig();
info(`User ${chat.username} updated default params: ${JSON.stringify(body.data)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
await setConfig({ defaultParams });
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
}, { admin: true });
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
info(`User ${chat.username} updated default params: ${JSON.stringify(body.data)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
await setConfig({ defaultParams });
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
},
),
});

View File

@ -1,11 +1,16 @@
import { createLoggerMiddleware, createPathFilter } from "t_rest/server";
import { botRoute } from "./botRoute.ts";
import {
createEndpoint,
createLoggerMiddleware,
createMethodFilter,
createPathFilter,
} from "t_rest/server";
import { jobsRoute } from "./jobsRoute.ts";
import { paramsRoute } from "./paramsRoute.ts";
import { sessionsRoute } from "./sessionsRoute.ts";
import { statsRoute } from "./statsRoute.ts";
import { usersRoute } from "./usersRoute.ts";
import { workersRoute } from "./workersRoute.ts";
import { bot } from "../bot/mod.ts";
export const serveApi = createLoggerMiddleware(
createPathFilter({
@ -15,7 +20,13 @@ export const serveApi = createLoggerMiddleware(
"settings/params": paramsRoute,
"stats": statsRoute,
"workers": workersRoute,
"bot": botRoute,
"bot": createMethodFilter({
// deno-lint-ignore require-await
GET: createEndpoint({ query: null, body: null }, async () => {
const username = bot.botInfo.username;
return { status: 200, body: { type: "application/json", data: { username } } };
}),
}),
}),
{ filterStatus: (status) => status >= 400 },
);

View File

@ -1,3 +1,4 @@
// deno-lint-ignore-file require-await
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { ulid } from "ulid";

View File

@ -1,28 +0,0 @@
import { Chat } from "grammy_types";
import { Output } from "t_rest/client";
import { getConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts";
import { sessions } from "./sessionsRoute.ts";
export async function withUser<O extends Output>(
query: { sessionId: string },
cb: (user: Chat.PrivateGetChat) => Promise<O>,
options?: { admin?: boolean },
) {
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } } as const;
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (options?.admin) {
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } } as const;
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } } as const;
}
}
return cb(chat);
}

View File

@ -3,16 +3,14 @@ import { Model } from "indexed_kv";
import createOpenApiFetch from "openapi_fetch";
import { info } from "std/log/mod.ts";
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { getConfig } from "../app/config.ts";
import { activeGenerationWorkers } from "../app/generationQueue.ts";
import { generationStore } from "../app/generationStore.ts";
import * as SdApi from "../app/sdApi.ts";
import {
WorkerInstance,
workerInstanceSchema,
workerInstanceStore,
} from "../app/workerInstanceStore.ts";
import { WorkerInstance, workerInstanceStore } from "../app/workerInstanceStore.ts";
import { bot } from "../bot/mod.ts";
import { getAuthHeader } from "../utils/getAuthHeader.ts";
import { withUser } from "./withUser.ts";
import { sessions } from "./sessionsRoute.ts";
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
id: string;
@ -71,6 +69,7 @@ export const workersRoute = createPathFilter({
async () => {
const workerInstances = await workerInstanceStore.getAll();
const workers = await Promise.all(workerInstances.map(getWorkerData));
return {
status: 200,
body: { type: "application/json", data: workers satisfies WorkerData[] },
@ -84,18 +83,51 @@ export const workersRoute = createPathFilter({
},
body: {
type: "application/json",
schema: workerInstanceSchema,
schema: {
type: "object",
properties: {
key: { type: "string" },
name: { type: ["string", "null"] },
sdUrl: { type: "string" },
sdAuth: {
type: ["object", "null"],
properties: {
user: { type: "string" },
password: { type: "string" },
},
required: ["user", "password"],
},
},
required: ["key", "name", "sdUrl", "sdAuth"],
},
},
},
async ({ query, body }) => {
return withUser(query, async (chat) => {
const workerInstance = await workerInstanceStore.create(body.data);
info(`User ${chat.username} created worker ${workerInstance.id}`);
return {
status: 200,
body: { type: "application/json", data: await getWorkerData(workerInstance) },
};
}, { admin: true });
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
const workerInstance = await workerInstanceStore.create({
key: body.data.key,
name: body.data.name,
sdUrl: body.data.sdUrl,
sdAuth: body.data.sdAuth,
});
info(`User ${chat.username} created worker ${workerInstance.id}`);
const worker = await getWorkerData(workerInstance);
return {
status: 200,
body: { type: "application/json", data: worker satisfies WorkerData },
};
},
),
}),
@ -109,9 +141,10 @@ export const workersRoute = createPathFilter({
if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
}
const worker: WorkerData = await getWorkerData(workerInstance);
return {
status: 200,
body: { type: "application/json", data: await getWorkerData(workerInstance) },
body: { type: "application/json", data: worker satisfies WorkerData },
};
},
),
@ -122,7 +155,22 @@ export const workersRoute = createPathFilter({
},
body: {
type: "application/json",
schema: { ...workerInstanceSchema, required: [] },
schema: {
type: "object",
properties: {
key: { type: "string" },
name: { type: ["string", "null"] },
sdUrl: { type: "string" },
auth: {
type: ["object", "null"],
properties: {
user: { type: "string" },
password: { type: "string" },
},
required: ["user", "password"],
},
},
},
},
},
async ({ params, query, body }) => {
@ -130,18 +178,37 @@ export const workersRoute = createPathFilter({
if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
}
return withUser(query, async (chat) => {
info(
`User ${chat.username} updated worker ${params.workerId}: ${
JSON.stringify(body.data)
}`,
);
await workerInstance.update(body.data);
return {
status: 200,
body: { type: "application/json", data: await getWorkerData(workerInstance) },
};
}, { admin: true });
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
if (body.data.name !== undefined) {
workerInstance.value.name = body.data.name;
}
if (body.data.sdUrl !== undefined) {
workerInstance.value.sdUrl = body.data.sdUrl;
}
if (body.data.auth !== undefined) {
workerInstance.value.sdAuth = body.data.auth;
}
info(
`User ${chat.username} updated worker ${params.workerId}: ${JSON.stringify(body.data)}`,
);
await workerInstance.update();
const worker = await getWorkerData(workerInstance);
return {
status: 200,
body: { type: "application/json", data: worker satisfies WorkerData },
};
},
),
DELETE: createEndpoint(
@ -156,11 +223,22 @@ export const workersRoute = createPathFilter({
if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
}
return withUser(query, async (chat) => {
info(`User ${chat.username} deleted worker ${params.workerId}`);
await workerInstance.delete();
return { status: 200, body: { type: "application/json", data: null } };
}, { admin: true });
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
info(`User ${chat.username} deleted worker ${params.workerId}`);
await workerInstance.delete();
return { status: 200, body: { type: "application/json", data: null } };
},
),
}),

View File

@ -9,13 +9,6 @@
"fmt": {
"lineWidth": 100
},
"lint": {
"rules": {
"exclude": [
"require-await"
]
}
},
"imports": {
"@date-fns/utc": "https://cdn.skypack.dev/@date-fns/utc@1.1.0?dts",
"async": "https://deno.land/x/async@v2.0.2/mod.ts",

View File

@ -249,7 +249,7 @@ function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
body: {
type: "application/json",
data: {
sdAuth: user && password ? { user, password } : null,
auth: user && password ? { user, password } : null,
},
},
});

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow: /