forked from pinks/eris
Compare commits
1 Commits
main
...
fix-pnginf
Author | SHA1 | Date |
---|---|---|
Vargoshi | 320924da91 |
|
@ -1,3 +1,3 @@
|
||||||
.env
|
.env
|
||||||
*.db
|
app*.db*
|
||||||
*.db-*
|
updateConfig.ts
|
||||||
|
|
38
README.md
38
README.md
|
@ -1,12 +1,11 @@
|
||||||
# Nyx the Bot
|
# Eris the Bot
|
||||||
Fork of Eris the Bot https://eris.lisq.eu
|
|
||||||
|
|
||||||
Telegram bot for generating images from text.
|
Telegram bot for generating images from text.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Deno](https://deno.land/) (for the bot server)
|
- [Deno](https://deno.land/)
|
||||||
- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/) (for the worker that generates images)
|
- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/)
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
@ -14,33 +13,22 @@ You can put these in `.env` file or pass them as environment variables.
|
||||||
|
|
||||||
- `TG_BOT_TOKEN` - Telegram bot token. Get yours from [@BotFather](https://t.me/BotFather).
|
- `TG_BOT_TOKEN` - Telegram bot token. Get yours from [@BotFather](https://t.me/BotFather).
|
||||||
Required.
|
Required.
|
||||||
- `DENO_KV_PATH` - [Deno KV](https://deno.land/api?s=Deno.openKv&unstable) database file path. A
|
- `TG_ADMIN_USERNAMES` - Comma separated list of usernames of users that can use admin commands.
|
||||||
temporary file is used by default. Example: /opt/data/botdata.kv
|
|
||||||
- `LOG_LEVEL` - [Log level](https://deno.land/std@0.201.0/log/mod.ts?s=LogLevels). Default: `INFO`.
|
- `LOG_LEVEL` - [Log level](https://deno.land/std@0.201.0/log/mod.ts?s=LogLevels). Default: `INFO`.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
1. Start Eris: `deno task start`
|
- Start stable diffusion webui: `cd sd-webui`, `./webui.sh --api`
|
||||||
2. Visit [Eris WebUI](http://localhost:8443/) and login via Telegram.
|
- Start bot: `deno task start`
|
||||||
3. Promote yourself to admin in the Eris WebUI.
|
|
||||||
4. Start Stable Diffusion WebUI: `./webui.sh --api` (in SD WebUI directory)
|
|
||||||
5. Add a new worker in the Eris WebUI.
|
|
||||||
|
|
||||||
## This fork requires the use of webhooks.
|
To connect your SD to the bot, open the [Eris UI](http://localhost:5999/), login as admin and add a
|
||||||
|
worker.
|
||||||
1. You need a reverse Proxy and HTTPS certificate set up that proxies all requests from a domain on port 443 (e.g. nyx.akiru.de) to the backend of this bot (:8443).
|
|
||||||
2. Change the Webhook URL to your own one in /mod/bot.ts - There are also console log statements you can uncomment for troubleshooting.
|
|
||||||
3. Make sure the DNS and firewall are set up for the webhook to be reachable, because otherwise the bot fails to start.
|
|
||||||
|
|
||||||
## Codegen
|
## Codegen
|
||||||
|
|
||||||
The Stable Diffusion API types are auto-generated. To regenerate them, first start your SD WebUI
|
The Stable Diffusion API in `app/sdApi.ts` is auto-generated. To regenerate it, first start your SD
|
||||||
with `--nowebui --api`, and then run `deno task generate`
|
WebUI with `--nowebui --api`, and then run:
|
||||||
|
|
||||||
## Project structure
|
```sh
|
||||||
|
deno run npm:openapi-typescript http://localhost:7861/openapi.json -o app/sdApi.ts
|
||||||
- `/api` - Eris API served at `http://localhost:8443/api/`.
|
```
|
||||||
- `/app` - Queue handling and other core processes.
|
|
||||||
- `/bot` - Handling bot commands and other updates from Telegram API.
|
|
||||||
- `/ui` - Eris WebUI frontend files served at `http://localhost:8443/`.
|
|
||||||
- `/util` - Utility functions shared by other parts.
|
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { info } from "std/log/mod.ts";
|
|
||||||
import { Elysia, Static, t } from "elysia";
|
|
||||||
import { Admin, adminSchema, adminStore } from "../app/adminStore.ts";
|
|
||||||
import { getUser, withSessionAdmin, withSessionUser } from "./getUser.ts";
|
|
||||||
import { TkvEntry } from "../utils/Tkv.ts";
|
|
||||||
|
|
||||||
const adminDataSchema = t.Intersect([adminSchema, t.Object({ tgUserId: t.Number() })]);
|
|
||||||
|
|
||||||
export type AdminData = Static<typeof adminDataSchema>;
|
|
||||||
|
|
||||||
function getAdminData(adminEntry: TkvEntry<["admins", number], Admin>): AdminData {
|
|
||||||
return { tgUserId: adminEntry.key[1], ...adminEntry.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const adminsRoute = new Elysia()
|
|
||||||
.get(
|
|
||||||
"",
|
|
||||||
async () => {
|
|
||||||
const adminEntries = await Array.fromAsync(adminStore.list({ prefix: ["admins"] }));
|
|
||||||
const admins = adminEntries.map(getAdminData);
|
|
||||||
return admins;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: t.Array(adminDataSchema),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"",
|
|
||||||
async ({ query, body, set }) => {
|
|
||||||
return withSessionAdmin({ query, set }, async (sessionUser, sessionAdminEntry) => {
|
|
||||||
const newAdminUser = await getUser(body.tgUserId);
|
|
||||||
const newAdminKey = ["admins", body.tgUserId] as const;
|
|
||||||
const newAdminValue = { promotedBy: sessionAdminEntry.key[1] };
|
|
||||||
const newAdminResult = await adminStore.atomicSet(newAdminKey, null, newAdminValue);
|
|
||||||
if (!newAdminResult.ok) {
|
|
||||||
set.status = 409;
|
|
||||||
return "User is already an admin";
|
|
||||||
}
|
|
||||||
info(`User ${sessionUser.first_name} promoted user ${newAdminUser.first_name} to admin`);
|
|
||||||
return getAdminData({ ...newAdminResult, key: newAdminKey, value: newAdminValue });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
body: t.Object({
|
|
||||||
tgUserId: t.Number(),
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: adminDataSchema,
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
409: t.Literal("User is already an admin"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"/promote_self",
|
|
||||||
// if there are no admins, allow any user to promote themselves
|
|
||||||
async ({ query, set }) => {
|
|
||||||
return withSessionUser({ query, set }, async (sessionUser) => {
|
|
||||||
const adminEntries = await Array.fromAsync(adminStore.list({ prefix: ["admins"] }));
|
|
||||||
if (adminEntries.length !== 0) {
|
|
||||||
set.status = 409;
|
|
||||||
return "You are not allowed to promote yourself";
|
|
||||||
}
|
|
||||||
const newAdminKey = ["admins", sessionUser.id] as const;
|
|
||||||
const newAdminValue = { promotedBy: null };
|
|
||||||
const newAdminResult = await adminStore.set(newAdminKey, newAdminValue);
|
|
||||||
info(`User ${sessionUser.first_name} promoted themselves to admin`);
|
|
||||||
return getAdminData({ ...newAdminResult, key: newAdminKey, value: newAdminValue });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
response: {
|
|
||||||
200: adminDataSchema,
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
409: t.Literal("You are not allowed to promote yourself"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/:adminId",
|
|
||||||
async ({ params, set }) => {
|
|
||||||
const adminEntry = await adminStore.get(["admins", Number(params.adminId)]);
|
|
||||||
if (!adminEntry.versionstamp) {
|
|
||||||
set.status = 404;
|
|
||||||
return "Admin not found";
|
|
||||||
}
|
|
||||||
return getAdminData(adminEntry);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: t.Object({ adminId: t.String() }),
|
|
||||||
response: {
|
|
||||||
200: adminDataSchema,
|
|
||||||
404: t.Literal("Admin not found"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.delete(
|
|
||||||
"/:adminId",
|
|
||||||
async ({ params, query, set }) => {
|
|
||||||
return withSessionAdmin({ query, set }, async (sessionUser) => {
|
|
||||||
const deletedAdminEntry = await adminStore.get(["admins", Number(params.adminId)]);
|
|
||||||
if (!deletedAdminEntry.versionstamp) {
|
|
||||||
set.status = 404;
|
|
||||||
return "Admin not found";
|
|
||||||
}
|
|
||||||
const deletedAdminUser = await getUser(deletedAdminEntry.key[1]);
|
|
||||||
await adminStore.delete(["admins", Number(params.adminId)]);
|
|
||||||
info(
|
|
||||||
`User ${sessionUser.first_name} demoted user ${deletedAdminUser.first_name} from admin`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: t.Object({ adminId: t.String() }),
|
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
response: {
|
|
||||||
200: t.Null(),
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
404: t.Literal("Admin not found"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Elysia, t } from "elysia";
|
|
||||||
import { bot } from "../bot/mod.ts";
|
|
||||||
|
|
||||||
export const botRoute = new Elysia()
|
|
||||||
.get(
|
|
||||||
"",
|
|
||||||
async () => {
|
|
||||||
const username = bot.botInfo.username;
|
|
||||||
return { username };
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: t.Object({ username: t.String() }),
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { Chat } from "grammy_types";
|
|
||||||
import { Admin, adminStore } from "../app/adminStore.ts";
|
|
||||||
import { bot } from "../bot/mod.ts";
|
|
||||||
import { sessions } from "./sessionsRoute.ts";
|
|
||||||
import { TkvEntry } from "../utils/Tkv.ts";
|
|
||||||
|
|
||||||
export async function withSessionUser<O>(
|
|
||||||
{ query, set }: { query: { sessionId: string }; set: { status?: string | number } },
|
|
||||||
cb: (sessionUser: Chat.PrivateGetChat) => Promise<O>,
|
|
||||||
) {
|
|
||||||
const session = sessions.get(query.sessionId);
|
|
||||||
if (!session?.userId) {
|
|
||||||
set.status = 401;
|
|
||||||
return "Must be logged in";
|
|
||||||
}
|
|
||||||
const user = await getUser(session.userId);
|
|
||||||
return cb(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function withSessionAdmin<O>(
|
|
||||||
{ query, set }: { query: { sessionId: string }; set: { status?: string | number } },
|
|
||||||
cb: (
|
|
||||||
sessionUser: Chat.PrivateGetChat,
|
|
||||||
sessionAdminEntry: TkvEntry<["admins", number], Admin>,
|
|
||||||
) => Promise<O>,
|
|
||||||
) {
|
|
||||||
const session = sessions.get(query.sessionId);
|
|
||||||
if (!session?.userId) {
|
|
||||||
set.status = 401;
|
|
||||||
return "Must be logged in";
|
|
||||||
}
|
|
||||||
const sessionUser = await getUser(session.userId);
|
|
||||||
const sessionAdminEntry = await adminStore.get(["admins", sessionUser.id]);
|
|
||||||
if (!sessionAdminEntry.versionstamp) {
|
|
||||||
set.status = 403;
|
|
||||||
return "Must be an admin";
|
|
||||||
}
|
|
||||||
return cb(sessionUser, sessionAdminEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUser(userId: number): Promise<Chat.PrivateGetChat> {
|
|
||||||
const chat = await bot.api.getChat(userId);
|
|
||||||
if (chat.type !== "private") throw new Error("Chat is not private");
|
|
||||||
return chat;
|
|
||||||
}
|
|
|
@ -1,40 +1,15 @@
|
||||||
import { Elysia, t } from "elysia";
|
import { createEndpoint, createMethodFilter } from "t_rest/server";
|
||||||
import { generationQueue } from "../app/generationQueue.ts";
|
import { generationQueue } from "../app/generationQueue.ts";
|
||||||
|
|
||||||
export const jobsRoute = new Elysia()
|
export const jobsRoute = createMethodFilter({
|
||||||
.get(
|
GET: createEndpoint(
|
||||||
"",
|
{ query: null, body: null },
|
||||||
async () => {
|
async () => ({
|
||||||
const allJobs = await generationQueue.getAllJobs();
|
status: 200,
|
||||||
return allJobs.map((job) => ({
|
body: {
|
||||||
id: job.id.join(":"),
|
type: "application/json",
|
||||||
place: job.place,
|
data: await generationQueue.getAllJobs(),
|
||||||
state: {
|
|
||||||
from: {
|
|
||||||
language_code: job.state.from.language_code ?? null,
|
|
||||||
first_name: job.state.from.first_name,
|
|
||||||
last_name: job.state.from.last_name ?? null,
|
|
||||||
username: job.state.from.username ?? null,
|
|
||||||
},
|
},
|
||||||
progress: job.state.progress ?? null,
|
|
||||||
workerInstanceKey: job.state.workerInstanceKey ?? null,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: t.Array(t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
place: t.Number(),
|
|
||||||
state: t.Object({
|
|
||||||
from: t.Object({
|
|
||||||
language_code: t.Nullable(t.String()),
|
|
||||||
first_name: t.String(),
|
|
||||||
last_name: t.Nullable(t.String()),
|
|
||||||
username: t.Nullable(t.String()),
|
|
||||||
}),
|
}),
|
||||||
progress: t.Nullable(t.Number()),
|
),
|
||||||
workerInstanceKey: t.Nullable(t.String()),
|
});
|
||||||
}),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
12
api/mod.ts
12
api/mod.ts
|
@ -1,16 +1,12 @@
|
||||||
import { route } from "reroute";
|
import { route } from "reroute";
|
||||||
import { serveSpa } from "serve_spa";
|
import { serveSpa } from "serve_spa";
|
||||||
import { api } from "./serveApi.ts";
|
import { serveApi } from "./serveApi.ts";
|
||||||
import { fromFileUrl } from "std/path/mod.ts";
|
import { fromFileUrl } from "std/path/mod.ts"
|
||||||
// New function that handles the webhook
|
|
||||||
import { handleWebhook } from "../bot/mod.ts";
|
|
||||||
|
|
||||||
|
|
||||||
export async function serveUi() {
|
export async function serveUi() {
|
||||||
const server = Deno.serve({ port: 8443 }, (request) =>
|
const server = Deno.serve({ port: 5999 }, (request) =>
|
||||||
route(request, {
|
route(request, {
|
||||||
"/api/*": (request) => api.fetch(request),
|
"/api/*": (request) => serveApi(request),
|
||||||
"/webhook": handleWebhook, // Create the webhook route handle
|
|
||||||
"/*": (request) =>
|
"/*": (request) =>
|
||||||
serveSpa(request, {
|
serveSpa(request, {
|
||||||
fsRoot: fromFileUrl(new URL("../ui/", import.meta.url)),
|
fsRoot: fromFileUrl(new URL("../ui/", import.meta.url)),
|
||||||
|
|
|
@ -1,39 +1,45 @@
|
||||||
import { Elysia, t } from "elysia";
|
import { deepMerge } from "std/collections/deep_merge.ts";
|
||||||
import { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
import { defaultParamsSchema, getConfig, setConfig } from "../app/config.ts";
|
import { createEndpoint, createMethodFilter } from "t_rest/server";
|
||||||
import { withSessionAdmin } from "./getUser.ts";
|
import { configSchema, getConfig, setConfig } from "../app/config.ts";
|
||||||
|
import { bot } from "../bot/mod.ts";
|
||||||
|
import { sessions } from "./sessionsRoute.ts";
|
||||||
|
|
||||||
export const paramsRoute = new Elysia()
|
export const paramsRoute = createMethodFilter({
|
||||||
.get(
|
GET: createEndpoint(
|
||||||
"",
|
{ query: null, body: null },
|
||||||
async () => {
|
async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
return config.defaultParams;
|
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
PATCH: createEndpoint(
|
||||||
{
|
{
|
||||||
response: {
|
query: { sessionId: { type: "string" } },
|
||||||
200: defaultParamsSchema,
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
schema: configSchema.properties.defaultParams,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
async ({ query, body }) => {
|
||||||
.patch(
|
const session = sessions.get(query.sessionId);
|
||||||
"",
|
if (!session?.userId) {
|
||||||
async ({ query, body, set }) => {
|
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
|
||||||
return withSessionAdmin({ query, set }, async (user) => {
|
}
|
||||||
|
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();
|
const config = await getConfig();
|
||||||
info(`User ${user.first_name} updated default params: ${JSON.stringify(body)}`);
|
if (!config?.adminUsernames?.includes(chat.username)) {
|
||||||
const defaultParams = { ...config.defaultParams, ...body };
|
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 });
|
await setConfig({ defaultParams });
|
||||||
return config.defaultParams;
|
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
body: defaultParamsSchema,
|
|
||||||
response: {
|
|
||||||
200: defaultParamsSchema,
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
import { Elysia } from "elysia";
|
import {
|
||||||
import { swagger } from "elysia/swagger";
|
createEndpoint,
|
||||||
import { adminsRoute } from "./adminsRoute.ts";
|
createLoggerMiddleware,
|
||||||
import { botRoute } from "./botRoute.ts";
|
createMethodFilter,
|
||||||
|
createPathFilter,
|
||||||
|
} from "t_rest/server";
|
||||||
import { jobsRoute } from "./jobsRoute.ts";
|
import { jobsRoute } from "./jobsRoute.ts";
|
||||||
import { paramsRoute } from "./paramsRoute.ts";
|
import { paramsRoute } from "./paramsRoute.ts";
|
||||||
import { sessionsRoute } from "./sessionsRoute.ts";
|
import { sessionsRoute } from "./sessionsRoute.ts";
|
||||||
import { statsRoute } from "./statsRoute.ts";
|
import { statsRoute } from "./statsRoute.ts";
|
||||||
import { usersRoute } from "./usersRoute.ts";
|
import { usersRoute } from "./usersRoute.ts";
|
||||||
import { workersRoute } from "./workersRoute.ts";
|
import { workersRoute } from "./workersRoute.ts";
|
||||||
|
import { bot } from "../bot/mod.ts";
|
||||||
|
|
||||||
export const api = new Elysia()
|
export const serveApi = createLoggerMiddleware(
|
||||||
.use(
|
createPathFilter({
|
||||||
swagger({
|
"jobs": jobsRoute,
|
||||||
path: "/docs",
|
"sessions": sessionsRoute,
|
||||||
swaggerOptions: { url: "docs/json" } as never,
|
"users": usersRoute,
|
||||||
documentation: {
|
"settings/params": paramsRoute,
|
||||||
info: { title: "Eris API", version: "0.1" },
|
"stats": statsRoute,
|
||||||
servers: [{ url: "/api" }],
|
"workers": workersRoute,
|
||||||
},
|
"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 } } };
|
||||||
}),
|
}),
|
||||||
)
|
}),
|
||||||
.group("/admins", (api) => api.use(adminsRoute))
|
}),
|
||||||
.group("/bot", (api) => api.use(botRoute))
|
{ filterStatus: (status) => status >= 400 },
|
||||||
.group("/jobs", (api) => api.use(jobsRoute))
|
);
|
||||||
.group("/sessions", (api) => api.use(sessionsRoute))
|
|
||||||
.group("/settings/params", (api) => api.use(paramsRoute))
|
|
||||||
.group("/stats", (api) => api.use(statsRoute))
|
|
||||||
.group("/users", (api) => api.use(usersRoute))
|
|
||||||
.group("/workers", (api) => api.use(workersRoute));
|
|
||||||
|
|
||||||
export type Api = typeof api;
|
export type ApiHandler = typeof serveApi;
|
||||||
|
|
|
@ -1,43 +1,37 @@
|
||||||
import { Elysia, NotFoundError, t } from "elysia";
|
// deno-lint-ignore-file require-await
|
||||||
|
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>();
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
userId?: number | undefined;
|
userId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sessionsRoute = new Elysia()
|
export const sessionsRoute = createPathFilter({
|
||||||
.post(
|
"": createMethodFilter({
|
||||||
"",
|
POST: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
async () => {
|
async () => {
|
||||||
const id = ulid();
|
const id = ulid();
|
||||||
const session: Session = {};
|
const session: Session = {};
|
||||||
sessions.set(id, session);
|
sessions.set(id, session);
|
||||||
return { id, userId: session.userId ?? null };
|
return { status: 200, body: { type: "application/json", data: { id, ...session } } };
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
response: t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
userId: t.Nullable(t.Number()),
|
|
||||||
}),
|
}),
|
||||||
},
|
|
||||||
)
|
"{sessionId}": createMethodFilter({
|
||||||
.get(
|
GET: createEndpoint(
|
||||||
"/:sessionId",
|
{ query: null, body: null },
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const id = params.sessionId!;
|
const id = params.sessionId;
|
||||||
const session = sessions.get(id);
|
const session = sessions.get(id);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundError("Session not found");
|
return { status: 401, body: { type: "text/plain", data: "Session not found" } };
|
||||||
}
|
}
|
||||||
return { id, userId: session.userId ?? null };
|
return { status: 200, body: { type: "application/json", data: { id, ...session } } };
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
params: t.Object({ sessionId: t.String() }),
|
|
||||||
response: t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
userId: t.Nullable(t.Number()),
|
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Elysia, t } from "elysia";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
import { subMinutes } from "date-fns";
|
|
||||||
import { dailyStatsSchema, getDailyStats } from "../app/dailyStatsStore.ts";
|
|
||||||
import { generationStore } from "../app/generationStore.ts";
|
|
||||||
import { globalStats } from "../app/globalStats.ts";
|
import { globalStats } from "../app/globalStats.ts";
|
||||||
import { getUserDailyStats, userDailyStatsSchema } from "../app/userDailyStatsStore.ts";
|
import { getDailyStats } from "../app/dailyStatsStore.ts";
|
||||||
import { getUserStats, userStatsSchema } from "../app/userStatsStore.ts";
|
import { getUserStats } from "../app/userStatsStore.ts";
|
||||||
import { withSessionAdmin } from "./getUser.ts";
|
import { getUserDailyStats } from "../app/userDailyStatsStore.ts";
|
||||||
|
import { generationStore } from "../app/generationStore.ts";
|
||||||
|
import { subMinutes } from "date-fns";
|
||||||
|
|
||||||
const STATS_INTERVAL_MIN = 3;
|
const STATS_INTERVAL_MIN = 3;
|
||||||
|
|
||||||
export const statsRoute = new Elysia()
|
export const statsRoute = createPathFilter({
|
||||||
.get(
|
"": createMethodFilter({
|
||||||
"",
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
async () => {
|
async () => {
|
||||||
const after = subMinutes(new Date(), STATS_INTERVAL_MIN);
|
const after = subMinutes(new Date(), STATS_INTERVAL_MIN);
|
||||||
const generations = await generationStore.getAll({ after });
|
const generations = await generationStore.getAll({ after });
|
||||||
|
@ -36,6 +36,10 @@ export const statsRoute = new Elysia()
|
||||||
.reduce((sum, pixelSteps) => sum + pixelSteps, 0) / STATS_INTERVAL_MIN;
|
.reduce((sum, pixelSteps) => sum + pixelSteps, 0) / STATS_INTERVAL_MIN;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
imageCount: globalStats.imageCount,
|
imageCount: globalStats.imageCount,
|
||||||
stepCount: globalStats.stepCount,
|
stepCount: globalStats.stepCount,
|
||||||
pixelCount: globalStats.pixelCount,
|
pixelCount: globalStats.pixelCount,
|
||||||
|
@ -45,90 +49,77 @@ export const statsRoute = new Elysia()
|
||||||
stepsPerMinute,
|
stepsPerMinute,
|
||||||
pixelsPerMinute,
|
pixelsPerMinute,
|
||||||
pixelStepsPerMinute,
|
pixelStepsPerMinute,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
response: {
|
|
||||||
200: t.Object({
|
|
||||||
imageCount: t.Number(),
|
|
||||||
stepCount: t.Number(),
|
|
||||||
pixelCount: t.Number(),
|
|
||||||
pixelStepCount: t.Number(),
|
|
||||||
userCount: t.Number(),
|
|
||||||
imagesPerMinute: t.Number(),
|
|
||||||
stepsPerMinute: t.Number(),
|
|
||||||
pixelsPerMinute: t.Number(),
|
|
||||||
pixelStepsPerMinute: t.Number(),
|
|
||||||
}),
|
}),
|
||||||
},
|
"daily/{year}/{month}/{day}": createMethodFilter({
|
||||||
},
|
GET: createEndpoint(
|
||||||
)
|
{ query: null, body: null },
|
||||||
.get(
|
|
||||||
"/daily/:year/:month/:day",
|
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
return getDailyStats(params.year, params.month, params.day);
|
const year = Number(params.year);
|
||||||
},
|
const month = Number(params.month);
|
||||||
{
|
const day = Number(params.day);
|
||||||
params: t.Object({
|
const stats = await getDailyStats(year, month, day);
|
||||||
year: t.Number(),
|
|
||||||
month: t.Number(),
|
|
||||||
day: t.Number(),
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: dailyStatsSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/users/:userId",
|
|
||||||
async ({ params }) => {
|
|
||||||
const userId = params.userId;
|
|
||||||
// deno-lint-ignore no-unused-vars
|
|
||||||
const { tagCountMap, ...stats } = await getUserStats(userId);
|
|
||||||
return stats;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: t.Object({ userId: t.Number() }),
|
|
||||||
response: {
|
|
||||||
200: t.Omit(userStatsSchema, ["tagCountMap"]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/users/:userId/tagcount",
|
|
||||||
async ({ params, query, set }) => {
|
|
||||||
return withSessionAdmin({ query, set }, async () => {
|
|
||||||
const stats = await getUserStats(params.userId);
|
|
||||||
return {
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
|
imageCount: stats.imageCount,
|
||||||
|
pixelCount: stats.pixelCount,
|
||||||
|
userCount: stats.userIds.length,
|
||||||
|
timestamp: stats.timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
"users/{userId}": createMethodFilter({
|
||||||
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
|
async ({ params }) => {
|
||||||
|
const userId = Number(params.userId);
|
||||||
|
const stats = await getUserStats(userId);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
|
imageCount: stats.imageCount,
|
||||||
|
pixelCount: stats.pixelCount,
|
||||||
tagCountMap: stats.tagCountMap,
|
tagCountMap: stats.tagCountMap,
|
||||||
timestamp: stats.timestamp,
|
timestamp: stats.timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
params: t.Object({ userId: t.Number() }),
|
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
response: {
|
|
||||||
200: t.Pick(userStatsSchema, ["tagCountMap", "timestamp"]),
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/users/:userId/daily/:year/:month/:day",
|
|
||||||
async ({ params }) => {
|
|
||||||
return getUserDailyStats(params.userId, params.year, params.month, params.day);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: t.Object({
|
|
||||||
userId: t.Number(),
|
|
||||||
year: t.Number(),
|
|
||||||
month: t.Number(),
|
|
||||||
day: t.Number(),
|
|
||||||
}),
|
}),
|
||||||
response: {
|
"users/{userId}/daily/{year}/{month}/{day}": createMethodFilter({
|
||||||
200: userDailyStatsSchema,
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
|
async ({ params }) => {
|
||||||
|
const userId = Number(params.userId);
|
||||||
|
const year = Number(params.year);
|
||||||
|
const month = Number(params.month);
|
||||||
|
const day = Number(params.day);
|
||||||
|
const stats = await getUserDailyStats(userId, year, month, day);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
|
imageCount: stats.imageCount,
|
||||||
|
pixelCount: stats.pixelCount,
|
||||||
|
timestamp: stats.timestamp,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
|
@ -1,55 +1,55 @@
|
||||||
import { Elysia, t } from "elysia";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
import { adminSchema, adminStore } from "../app/adminStore.ts";
|
import { getConfig } from "../app/config.ts";
|
||||||
import { bot } from "../bot/mod.ts";
|
import { bot } from "../bot/mod.ts";
|
||||||
import { getUser } from "./getUser.ts";
|
|
||||||
|
|
||||||
export const usersRoute = new Elysia()
|
export const usersRoute = createPathFilter({
|
||||||
.get(
|
"{userId}/photo": createMethodFilter({
|
||||||
"/:userId/photo",
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const user = await getUser(Number(params.userId));
|
const chat = await bot.api.getChat(params.userId);
|
||||||
if (!user.photo) {
|
if (chat.type !== "private") {
|
||||||
throw new Error("User has no photo");
|
throw new Error("Chat is not private");
|
||||||
}
|
}
|
||||||
const photoFile = await bot.api.getFile(user.photo.small_file_id);
|
const photoData = chat.photo?.small_file_id
|
||||||
const photoData = await fetch(
|
? await fetch(
|
||||||
`https://api.telegram.org/file/bot${bot.token}/${photoFile.file_path}`,
|
`https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile(
|
||||||
).then((resp) => {
|
chat.photo.small_file_id,
|
||||||
if (!resp.ok) {
|
).then((file) => file.file_path)}`,
|
||||||
throw new Error("Failed to fetch photo");
|
).then((resp) => resp.arrayBuffer())
|
||||||
|
: undefined;
|
||||||
|
if (!photoData) {
|
||||||
|
return { status: 404, body: { type: "text/plain", data: "User has no photo" } };
|
||||||
}
|
}
|
||||||
return resp;
|
|
||||||
}).then((resp) => resp.arrayBuffer());
|
|
||||||
|
|
||||||
return new Response(new File([photoData], "avatar.jpg", { type: "image/jpeg" }));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: t.Object({ userId: t.String() }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/:userId",
|
|
||||||
async ({ params }) => {
|
|
||||||
const user = await getUser(Number(params.userId));
|
|
||||||
const adminEntry = await adminStore.get(["admins", user.id]);
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
status: 200,
|
||||||
first_name: user.first_name,
|
body: {
|
||||||
last_name: user.last_name ?? null,
|
type: "image/jpeg",
|
||||||
username: user.username ?? null,
|
data: new Blob([photoData], { type: "image/jpeg" }),
|
||||||
bio: user.bio ?? null,
|
},
|
||||||
admin: adminEntry.value ?? null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
params: t.Object({ userId: t.String() }),
|
|
||||||
response: t.Object({
|
|
||||||
id: t.Number(),
|
|
||||||
first_name: t.String(),
|
|
||||||
last_name: t.Nullable(t.String()),
|
|
||||||
username: t.Nullable(t.String()),
|
|
||||||
bio: t.Nullable(t.String()),
|
|
||||||
admin: t.Nullable(adminSchema),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
"{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 config = await getConfig();
|
||||||
|
const isAdmin = chat.username && config?.adminUsernames?.includes(chat.username);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: { ...chat, isAdmin },
|
||||||
},
|
},
|
||||||
);
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
|
@ -2,38 +2,28 @@ import { subMinutes } from "date-fns";
|
||||||
import { Model } from "indexed_kv";
|
import { Model } from "indexed_kv";
|
||||||
import createOpenApiFetch from "openapi_fetch";
|
import createOpenApiFetch from "openapi_fetch";
|
||||||
import { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
import { Elysia, NotFoundError, Static, t } from "elysia";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
|
import { getConfig } from "../app/config.ts";
|
||||||
import { activeGenerationWorkers } from "../app/generationQueue.ts";
|
import { activeGenerationWorkers } from "../app/generationQueue.ts";
|
||||||
import { generationStore } from "../app/generationStore.ts";
|
import { generationStore } from "../app/generationStore.ts";
|
||||||
import * as SdApi from "../app/sdApi.ts";
|
import * as SdApi from "../app/sdApi.ts";
|
||||||
import {
|
import { WorkerInstance, workerInstanceStore } from "../app/workerInstanceStore.ts";
|
||||||
WorkerInstance,
|
import { bot } from "../bot/mod.ts";
|
||||||
workerInstanceSchema,
|
|
||||||
workerInstanceStore,
|
|
||||||
} from "../app/workerInstanceStore.ts";
|
|
||||||
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
import { sessions } from "./sessionsRoute.ts";
|
||||||
import { withSessionAdmin } from "./getUser.ts";
|
|
||||||
|
|
||||||
const workerResponseSchema = t.Intersect([
|
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
|
||||||
t.Object({ id: t.String() }),
|
id: string;
|
||||||
t.Omit(workerInstanceSchema, ["sdUrl", "sdAuth"]),
|
isActive: boolean;
|
||||||
t.Object({
|
imagesPerMinute: number;
|
||||||
isActive: t.Boolean(),
|
stepsPerMinute: number;
|
||||||
imagesPerMinute: t.Number(),
|
pixelsPerMinute: number;
|
||||||
stepsPerMinute: t.Number(),
|
pixelStepsPerMinute: number;
|
||||||
pixelsPerMinute: t.Number(),
|
};
|
||||||
pixelStepsPerMinute: t.Number(),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type WorkerResponse = Static<typeof workerResponseSchema>;
|
|
||||||
|
|
||||||
const workerRequestSchema = t.Omit(workerInstanceSchema, ["lastOnlineTime", "lastError"]);
|
|
||||||
|
|
||||||
const STATS_INTERVAL_MIN = 10;
|
const STATS_INTERVAL_MIN = 10;
|
||||||
|
|
||||||
async function getWorkerResponse(workerInstance: Model<WorkerInstance>): Promise<WorkerResponse> {
|
async function getWorkerData(workerInstance: Model<WorkerInstance>): Promise<WorkerData> {
|
||||||
const after = subMinutes(new Date(), STATS_INTERVAL_MIN);
|
const after = subMinutes(new Date(), STATS_INTERVAL_MIN);
|
||||||
|
|
||||||
const generations = await generationStore.getBy("workerInstanceKey", {
|
const generations = await generationStore.getBy("workerInstanceKey", {
|
||||||
|
@ -58,7 +48,7 @@ async function getWorkerResponse(workerInstance: Model<WorkerInstance>): Promise
|
||||||
)
|
)
|
||||||
.reduce((sum, pixelSteps) => sum + pixelSteps, 0) / STATS_INTERVAL_MIN;
|
.reduce((sum, pixelSteps) => sum + pixelSteps, 0) / STATS_INTERVAL_MIN;
|
||||||
|
|
||||||
return omitUndef({
|
return {
|
||||||
id: workerInstance.id,
|
id: workerInstance.id,
|
||||||
key: workerInstance.value.key,
|
key: workerInstance.value.key,
|
||||||
name: workerInstance.value.name,
|
name: workerInstance.value.name,
|
||||||
|
@ -69,113 +59,197 @@ async function getWorkerResponse(workerInstance: Model<WorkerInstance>): Promise
|
||||||
stepsPerMinute,
|
stepsPerMinute,
|
||||||
pixelsPerMinute,
|
pixelsPerMinute,
|
||||||
pixelStepsPerMinute,
|
pixelStepsPerMinute,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workersRoute = new Elysia()
|
export const workersRoute = createPathFilter({
|
||||||
.get(
|
"": createMethodFilter({
|
||||||
"",
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
async () => {
|
async () => {
|
||||||
const workerInstances = await workerInstanceStore.getAll();
|
const workerInstances = await workerInstanceStore.getAll();
|
||||||
const workers = await Promise.all(workerInstances.map(getWorkerResponse));
|
const workers = await Promise.all(workerInstances.map(getWorkerData));
|
||||||
return workers;
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: workers satisfies WorkerData[] },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
POST: createEndpoint(
|
||||||
{
|
{
|
||||||
response: t.Array(workerResponseSchema),
|
query: {
|
||||||
|
sessionId: { type: "string" },
|
||||||
},
|
},
|
||||||
)
|
body: {
|
||||||
.post(
|
type: "application/json",
|
||||||
"",
|
schema: {
|
||||||
async ({ query, body, set }) => {
|
type: "object",
|
||||||
return withSessionAdmin({ query, set }, async (sessionUser) => {
|
properties: {
|
||||||
const workerInstance = await workerInstanceStore.create(body);
|
key: { type: "string" },
|
||||||
info(`User ${sessionUser.first_name} created worker ${workerInstance.value.name}`);
|
name: { type: ["string", "null"] },
|
||||||
return await getWorkerResponse(workerInstance);
|
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 }) => {
|
||||||
|
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 },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
query: t.Object({ sessionId: t.String() }),
|
}),
|
||||||
body: workerRequestSchema,
|
|
||||||
response: {
|
"{workerId}": createPathFilter({
|
||||||
200: workerResponseSchema,
|
"": createMethodFilter({
|
||||||
401: t.Literal("Must be logged in"),
|
GET: createEndpoint(
|
||||||
403: t.Literal("Must be an admin"),
|
{ query: null, body: null },
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/:workerId",
|
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
throw new NotFoundError("Worker not found");
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
return await getWorkerResponse(workerInstance);
|
const worker: WorkerData = await getWorkerData(workerInstance);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: worker satisfies WorkerData },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
PATCH: createEndpoint(
|
||||||
{
|
{
|
||||||
params: t.Object({ workerId: t.String() }),
|
query: {
|
||||||
response: {
|
sessionId: { type: "string" },
|
||||||
200: workerResponseSchema,
|
},
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
},
|
||||||
.patch(
|
},
|
||||||
"/:workerId",
|
},
|
||||||
async ({ params, query, body, set }) => {
|
async ({ params, query, body }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
throw new NotFoundError("Worker not found");
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
return withSessionAdmin({ query, set }, async (sessionUser) => {
|
|
||||||
info(
|
info(
|
||||||
`User ${sessionUser.first_name} updated worker ${workerInstance.value.name}: ${
|
`User ${chat.username} updated worker ${params.workerId}: ${JSON.stringify(body.data)}`,
|
||||||
JSON.stringify(body)
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
await workerInstance.update(body);
|
await workerInstance.update();
|
||||||
return await getWorkerResponse(workerInstance);
|
const worker = await getWorkerData(workerInstance);
|
||||||
});
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: worker satisfies WorkerData },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
DELETE: createEndpoint(
|
||||||
{
|
{
|
||||||
params: t.Object({ workerId: t.String() }),
|
query: {
|
||||||
query: t.Object({ sessionId: t.String() }),
|
sessionId: { type: "string" },
|
||||||
body: t.Partial(workerRequestSchema),
|
|
||||||
response: {
|
|
||||||
200: workerResponseSchema,
|
|
||||||
401: t.Literal("Must be logged in"),
|
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
},
|
},
|
||||||
|
body: null,
|
||||||
},
|
},
|
||||||
)
|
async ({ params, query }) => {
|
||||||
.delete(
|
|
||||||
"/:workerId",
|
|
||||||
async ({ params, query, set }) => {
|
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
throw new Error("Worker not found");
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
return withSessionAdmin({ query, set }, async (sessionUser) => {
|
const session = sessions.get(query.sessionId);
|
||||||
info(`User ${sessionUser.first_name} deleted worker ${workerInstance.value.name}`);
|
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();
|
await workerInstance.delete();
|
||||||
return null;
|
return { status: 200, body: { type: "application/json", data: null } };
|
||||||
});
|
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
params: t.Object({ workerId: t.String() }),
|
}),
|
||||||
query: t.Object({ sessionId: t.String() }),
|
|
||||||
response: {
|
"loras": createMethodFilter({
|
||||||
200: t.Null(),
|
GET: createEndpoint(
|
||||||
401: t.Literal("Must be logged in"),
|
{ query: null, body: null },
|
||||||
403: t.Literal("Must be an admin"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/:workerId/loras",
|
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
throw new NotFoundError("Worker not found");
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
const sdClient = createOpenApiFetch<SdApi.paths>({
|
const sdClient = createOpenApiFetch<SdApi.paths>({
|
||||||
baseUrl: workerInstance.value.sdUrl,
|
baseUrl: workerInstance.value.sdUrl,
|
||||||
|
@ -183,32 +257,30 @@ export const workersRoute = new Elysia()
|
||||||
});
|
});
|
||||||
const lorasResponse = await sdClient.GET("/sdapi/v1/loras", {});
|
const lorasResponse = await sdClient.GET("/sdapi/v1/loras", {});
|
||||||
if (lorasResponse.error) {
|
if (lorasResponse.error) {
|
||||||
throw new Error(
|
return {
|
||||||
`Loras request failed: ${lorasResponse["error"]}`,
|
status: 500,
|
||||||
);
|
body: { type: "text/plain", data: `Loras request failed: ${lorasResponse["error"]}` },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const loras = (lorasResponse.data as Lora[]).map((lora) => ({
|
const loras = (lorasResponse.data as Lora[]).map((lora) => ({
|
||||||
name: lora.name,
|
name: lora.name,
|
||||||
alias: lora.alias ?? null,
|
alias: lora.alias ?? null,
|
||||||
}));
|
}));
|
||||||
return loras;
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: loras },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
|
||||||
params: t.Object({ workerId: t.String() }),
|
|
||||||
response: t.Array(
|
|
||||||
t.Object({
|
|
||||||
name: t.String(),
|
|
||||||
alias: t.Nullable(t.String()),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
},
|
}),
|
||||||
)
|
|
||||||
.get(
|
"models": createMethodFilter({
|
||||||
"/:workerId/models",
|
GET: createEndpoint(
|
||||||
|
{ query: null, body: null },
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
throw new NotFoundError("Worker not found");
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
const sdClient = createOpenApiFetch<SdApi.paths>({
|
const sdClient = createOpenApiFetch<SdApi.paths>({
|
||||||
baseUrl: workerInstance.value.sdUrl,
|
baseUrl: workerInstance.value.sdUrl,
|
||||||
|
@ -216,30 +288,29 @@ export const workersRoute = new Elysia()
|
||||||
});
|
});
|
||||||
const modelsResponse = await sdClient.GET("/sdapi/v1/sd-models", {});
|
const modelsResponse = await sdClient.GET("/sdapi/v1/sd-models", {});
|
||||||
if (modelsResponse.error) {
|
if (modelsResponse.error) {
|
||||||
throw new Error(
|
return {
|
||||||
`Models request failed: ${modelsResponse["error"]}`,
|
status: 500,
|
||||||
);
|
body: {
|
||||||
|
type: "text/plain",
|
||||||
|
data: `Models request failed: ${modelsResponse["error"]}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const models = modelsResponse.data.map((model) => ({
|
const models = modelsResponse.data.map((model) => ({
|
||||||
title: model.title,
|
title: model.title,
|
||||||
modelName: model.model_name,
|
modelName: model.model_name,
|
||||||
hash: model.hash ?? null,
|
hash: model.hash,
|
||||||
sha256: model.sha256 ?? null,
|
sha256: model.sha256,
|
||||||
}));
|
}));
|
||||||
return models;
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: models },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
|
||||||
params: t.Object({ workerId: t.String() }),
|
|
||||||
response: t.Array(
|
|
||||||
t.Object({
|
|
||||||
title: t.String(),
|
|
||||||
modelName: t.String(),
|
|
||||||
hash: t.Nullable(t.String()),
|
|
||||||
sha256: t.Nullable(t.String()),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
},
|
}),
|
||||||
);
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export interface Lora {
|
export interface Lora {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { Static, t } from "elysia";
|
|
||||||
import { db } from "./db.ts";
|
|
||||||
import { Tkv } from "../utils/Tkv.ts";
|
|
||||||
|
|
||||||
export const adminSchema = t.Object({
|
|
||||||
promotedBy: t.Nullable(t.Number()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Admin = Static<typeof adminSchema>;
|
|
||||||
|
|
||||||
export const adminStore = new Tkv<["admins", number], Admin>(db);
|
|
|
@ -1,45 +1,52 @@
|
||||||
import { Static, t } from "elysia";
|
|
||||||
import { Tkv } from "../utils/Tkv.ts";
|
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
|
|
||||||
export const defaultParamsSchema = t.Partial(t.Object({
|
export const configSchema = {
|
||||||
batch_size: t.Number(),
|
type: "object",
|
||||||
n_iter: t.Number(),
|
properties: {
|
||||||
width: t.Number(),
|
adminUsernames: { type: "array", items: { type: "string" } },
|
||||||
height: t.Number(),
|
pausedReason: { type: ["string", "null"] },
|
||||||
steps: t.Number(),
|
maxUserJobs: { type: "number" },
|
||||||
cfg_scale: t.Number(),
|
maxJobs: { type: "number" },
|
||||||
sampler_name: t.String(),
|
defaultParams: {
|
||||||
negative_prompt: t.String(),
|
type: "object",
|
||||||
}));
|
properties: {
|
||||||
|
batch_size: { type: "number" },
|
||||||
|
n_iter: { type: "number" },
|
||||||
|
width: { type: "number" },
|
||||||
|
height: { type: "number" },
|
||||||
|
steps: { type: "number" },
|
||||||
|
cfg_scale: { type: "number" },
|
||||||
|
sampler_name: { type: "string" },
|
||||||
|
negative_prompt: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["adminUsernames", "maxUserJobs", "maxJobs", "defaultParams"],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
export type DefaultParams = Static<typeof defaultParamsSchema>;
|
export type Config = jsonType<typeof configSchema>;
|
||||||
|
|
||||||
export const configSchema = t.Object({
|
|
||||||
pausedReason: t.Nullable(t.String()),
|
|
||||||
maxUserJobs: t.Number(),
|
|
||||||
maxJobs: t.Number(),
|
|
||||||
defaultParams: defaultParamsSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Config = Static<typeof configSchema>;
|
|
||||||
|
|
||||||
export const configStore = new Tkv<["config"], Config>(db);
|
|
||||||
|
|
||||||
const defaultConfig: Config = {
|
|
||||||
pausedReason: null,
|
|
||||||
maxUserJobs: Infinity,
|
|
||||||
maxJobs: Infinity,
|
|
||||||
defaultParams: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getConfig(): Promise<Config> {
|
export async function getConfig(): Promise<Config> {
|
||||||
const configEntry = await configStore.get(["config"]);
|
const configEntry = await db.get<Config>(["config"]);
|
||||||
return { ...defaultConfig, ...configEntry.value };
|
const config = configEntry?.value;
|
||||||
|
return {
|
||||||
|
adminUsernames: config?.adminUsernames ?? Deno.env.get("TG_ADMIN_USERNAMES")?.split(",") ?? [],
|
||||||
|
pausedReason: config?.pausedReason ?? null,
|
||||||
|
maxUserJobs: config?.maxUserJobs ?? Infinity,
|
||||||
|
maxJobs: config?.maxJobs ?? Infinity,
|
||||||
|
defaultParams: config?.defaultParams ?? {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setConfig<K extends keyof Config>(newConfig: Pick<Config, K>): Promise<void> {
|
export async function setConfig(newConfig: Partial<Config>): Promise<void> {
|
||||||
const configEntry = await configStore.get(["config"]);
|
const oldConfig = await getConfig();
|
||||||
const config = { ...defaultConfig, ...configEntry.value, ...newConfig };
|
const config: Config = {
|
||||||
await configStore.atomicSet(["config"], configEntry.versionstamp, config);
|
adminUsernames: newConfig.adminUsernames ?? oldConfig.adminUsernames,
|
||||||
|
pausedReason: newConfig.pausedReason ?? oldConfig.pausedReason,
|
||||||
|
maxUserJobs: newConfig.maxUserJobs ?? oldConfig.maxUserJobs,
|
||||||
|
maxJobs: newConfig.maxJobs ?? oldConfig.maxJobs,
|
||||||
|
defaultParams: newConfig.defaultParams ?? oldConfig.defaultParams,
|
||||||
|
};
|
||||||
|
await db.set(["config"], config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
|
import { UTCDateMini } from "@date-fns/utc";
|
||||||
import { hoursToMilliseconds, isSameDay, minutesToMilliseconds } from "date-fns";
|
import { hoursToMilliseconds, isSameDay, minutesToMilliseconds } from "date-fns";
|
||||||
import { UTCDateMini } from "date-fns/utc";
|
|
||||||
import { Static, t } from "elysia";
|
|
||||||
import { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
import { generationStore } from "./generationStore.ts";
|
import { generationStore } from "./generationStore.ts";
|
||||||
import { kvMemoize } from "../utils/kvMemoize.ts";
|
import { kvMemoize } from "./kvMemoize.ts";
|
||||||
|
|
||||||
export const dailyStatsSchema = t.Object({
|
export const dailyStatsSchema = {
|
||||||
userIds: t.Array(t.Number()),
|
type: "object",
|
||||||
imageCount: t.Number(),
|
properties: {
|
||||||
stepCount: t.Number(),
|
userIds: { type: "array", items: { type: "number" } },
|
||||||
pixelCount: t.Number(),
|
imageCount: { type: "number" },
|
||||||
pixelStepCount: t.Number(),
|
stepCount: { type: "number" },
|
||||||
timestamp: t.Number(),
|
pixelCount: { type: "number" },
|
||||||
});
|
pixelStepCount: { type: "number" },
|
||||||
|
timestamp: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["userIds", "imageCount", "stepCount", "pixelCount", "pixelStepCount", "timestamp"],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
export type DailyStats = Static<typeof dailyStatsSchema>;
|
export type DailyStats = jsonType<typeof dailyStatsSchema>;
|
||||||
|
|
||||||
export const getDailyStats = kvMemoize(
|
export const getDailyStats = kvMemoize(
|
||||||
db,
|
db,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { KvFs } from "kvfs";
|
import { KvFs } from "kvfs";
|
||||||
|
|
||||||
export const db = await Deno.openKv(Deno.env.get("DENO_KV_PATH"));
|
export const db = await Deno.openKv("./app.db");
|
||||||
export const fs = new KvFs(db);
|
export const fs = new KvFs(db);
|
||||||
|
|
|
@ -4,14 +4,13 @@ import { JobData, Queue, Worker } from "kvmq";
|
||||||
import createOpenApiClient from "openapi_fetch";
|
import createOpenApiClient from "openapi_fetch";
|
||||||
import { delay } from "std/async/delay.ts";
|
import { delay } from "std/async/delay.ts";
|
||||||
import { decode, encode } from "std/encoding/base64.ts";
|
import { decode, encode } from "std/encoding/base64.ts";
|
||||||
import { debug, error, info, warning } from "std/log/mod.ts";
|
import { debug, error, info } from "std/log/mod.ts";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { bot } from "../bot/mod.ts";
|
import { bot } from "../bot/mod.ts";
|
||||||
import { PngInfo } from "../bot/parsePngInfo.ts";
|
import { PngInfo } from "../bot/parsePngInfo.ts";
|
||||||
import { formatOrdinal } from "../utils/formatOrdinal.ts";
|
import { formatOrdinal } from "../utils/formatOrdinal.ts";
|
||||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||||
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { SdError } from "./SdError.ts";
|
import { SdError } from "./SdError.ts";
|
||||||
import { getConfig } from "./config.ts";
|
import { getConfig } from "./config.ts";
|
||||||
import { db, fs } from "./db.ts";
|
import { db, fs } from "./db.ts";
|
||||||
|
@ -70,8 +69,7 @@ export async function processGenerationQueue() {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const cleanedErrorMessage = error.message.replace(/url \([^)]+\)/, "");
|
workerInstance.update({ lastError: { message: error.message, time: Date.now() } })
|
||||||
workerInstance.update({ lastError: { message: cleanedErrorMessage, time: Date.now() } })
|
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
debug(`Worker ${workerInstance.value.key} is down: ${error}`);
|
debug(`Worker ${workerInstance.value.key} is down: ${error}`);
|
||||||
});
|
});
|
||||||
|
@ -163,7 +161,7 @@ async function processGenerationJob(
|
||||||
|
|
||||||
// reduce size if worker can't handle the resolution
|
// reduce size if worker can't handle the resolution
|
||||||
const size = limitSize(
|
const size = limitSize(
|
||||||
omitUndef({ ...config.defaultParams, ...state.task.params }),
|
{ ...config.defaultParams, ...state.task.params },
|
||||||
1024 * 1024,
|
1024 * 1024,
|
||||||
);
|
);
|
||||||
function limitSize(
|
function limitSize(
|
||||||
|
@ -184,18 +182,18 @@ async function processGenerationJob(
|
||||||
// start generating the image
|
// start generating the image
|
||||||
const responsePromise = state.task.type === "txt2img"
|
const responsePromise = state.task.type === "txt2img"
|
||||||
? workerSdClient.POST("/sdapi/v1/txt2img", {
|
? workerSdClient.POST("/sdapi/v1/txt2img", {
|
||||||
body: omitUndef({
|
body: {
|
||||||
...config.defaultParams,
|
...config.defaultParams,
|
||||||
...state.task.params,
|
...state.task.params,
|
||||||
...size,
|
...size,
|
||||||
negative_prompt: state.task.params.negative_prompt
|
negative_prompt: state.task.params.negative_prompt
|
||||||
? state.task.params.negative_prompt
|
? state.task.params.negative_prompt
|
||||||
: config.defaultParams?.negative_prompt,
|
: config.defaultParams?.negative_prompt,
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
: state.task.type === "img2img"
|
: state.task.type === "img2img"
|
||||||
? workerSdClient.POST("/sdapi/v1/img2img", {
|
? workerSdClient.POST("/sdapi/v1/img2img", {
|
||||||
body: omitUndef({
|
body: {
|
||||||
...config.defaultParams,
|
...config.defaultParams,
|
||||||
...state.task.params,
|
...state.task.params,
|
||||||
...size,
|
...size,
|
||||||
|
@ -211,7 +209,7 @@ async function processGenerationJob(
|
||||||
).then((resp) => resp.arrayBuffer()),
|
).then((resp) => resp.arrayBuffer()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -241,6 +239,8 @@ async function processGenerationJob(
|
||||||
if (progressResponse.data.progress > state.progress) {
|
if (progressResponse.data.progress > state.progress) {
|
||||||
state.progress = progressResponse.data.progress;
|
state.progress = progressResponse.data.progress;
|
||||||
await updateJob({ state: state });
|
await updateJob({ state: state });
|
||||||
|
await bot.api.sendChatAction(state.chat.id, "upload_photo", { maxAttempts: 1 })
|
||||||
|
.catch(() => undefined);
|
||||||
await bot.api.editMessageText(
|
await bot.api.editMessageText(
|
||||||
state.replyMessage.chat.id,
|
state.replyMessage.chat.id,
|
||||||
state.replyMessage.message_id,
|
state.replyMessage.message_id,
|
||||||
|
@ -288,24 +288,14 @@ async function processGenerationJob(
|
||||||
info,
|
info,
|
||||||
}, { retryCount: 5, retryDelayMs: 10000 });
|
}, { retryCount: 5, retryDelayMs: 10000 });
|
||||||
|
|
||||||
const uploadQueueSize = await uploadQueue.getAllJobs().then((jobs) =>
|
|
||||||
jobs.filter((job) => job.status !== "processing").length
|
|
||||||
);
|
|
||||||
|
|
||||||
// change status message to uploading images
|
// change status message to uploading images
|
||||||
await bot.api.editMessageText(
|
await bot.api.editMessageText(
|
||||||
state.replyMessage.chat.id,
|
state.replyMessage.chat.id,
|
||||||
state.replyMessage.message_id,
|
state.replyMessage.message_id,
|
||||||
uploadQueueSize > 10
|
`Uploading your images...`,
|
||||||
? `You are ${formatOrdinal(uploadQueueSize)} in upload queue.`
|
|
||||||
: `Uploading your images...`,
|
|
||||||
{ maxAttempts: 1 },
|
{ maxAttempts: 1 },
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
|
|
||||||
// send upload photo action
|
|
||||||
await bot.api.sendChatAction(state.chat.id, "upload_photo", { maxAttempts: 1 })
|
|
||||||
.catch(() => undefined);
|
|
||||||
|
|
||||||
debug(`Generation finished for ${formatUserChat(state)}`);
|
debug(`Generation finished for ${formatUserChat(state)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,31 +305,24 @@ async function processGenerationJob(
|
||||||
export async function updateGenerationQueue() {
|
export async function updateGenerationQueue() {
|
||||||
while (true) {
|
while (true) {
|
||||||
const jobs = await generationQueue.getAllJobs();
|
const jobs = await generationQueue.getAllJobs();
|
||||||
|
let index = 0;
|
||||||
const editedMessages = await Promise.all(jobs.map(async (job) => {
|
for (const job of jobs) {
|
||||||
if (job.status === "processing") {
|
if (job.lockUntil > new Date()) {
|
||||||
// if the job is processing, the worker will update its status message
|
// job is currently being processed, the worker will update its status message
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!job.state.replyMessage) {
|
||||||
// spread the updates in time randomly
|
// no status message, nothing to update
|
||||||
await delay(Math.random() * 3_000);
|
continue;
|
||||||
|
}
|
||||||
return await bot.api.editMessageText(
|
index++;
|
||||||
|
await bot.api.editMessageText(
|
||||||
job.state.replyMessage.chat.id,
|
job.state.replyMessage.chat.id,
|
||||||
job.state.replyMessage.message_id,
|
job.state.replyMessage.message_id,
|
||||||
`You are ${formatOrdinal(job.place)} in queue.`,
|
`You are ${formatOrdinal(index)} in queue.`,
|
||||||
{ maxAttempts: 1 },
|
{ maxAttempts: 1 },
|
||||||
).then(() => true).catch(() => false);
|
).catch(() => undefined);
|
||||||
}));
|
|
||||||
|
|
||||||
const erroredMessages = editedMessages.filter((ok) => !ok);
|
|
||||||
if (erroredMessages.length > 0) {
|
|
||||||
warning(
|
|
||||||
`Updating queue status failed for ${erroredMessages.length} / ${editedMessages.length} jobs`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
await delay(3000);
|
||||||
await delay(10_000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import { db } from "./db.ts";
|
||||||
export interface GenerationSchema {
|
export interface GenerationSchema {
|
||||||
from: User;
|
from: User;
|
||||||
chat: Chat;
|
chat: Chat;
|
||||||
sdInstanceId?: string | undefined; // TODO: change to workerInstanceKey
|
sdInstanceId?: string; // TODO: change to workerInstanceKey
|
||||||
info?: SdGenerationInfo | undefined;
|
info?: SdGenerationInfo;
|
||||||
startDate?: Date | undefined;
|
startDate?: Date;
|
||||||
endDate?: Date | undefined;
|
endDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
import { addDays } from "date-fns";
|
import { addDays } from "date-fns";
|
||||||
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
import { getDailyStats } from "./dailyStatsStore.ts";
|
import { getDailyStats } from "./dailyStatsStore.ts";
|
||||||
import { generationStore } from "./generationStore.ts";
|
import { generationStore } from "./generationStore.ts";
|
||||||
|
|
||||||
export interface GlobalStats {
|
export const globalStatsSchema = {
|
||||||
userIds: number[];
|
type: "object",
|
||||||
imageCount: number;
|
properties: {
|
||||||
stepCount: number;
|
userIds: { type: "array", items: { type: "number" } },
|
||||||
pixelCount: number;
|
imageCount: { type: "number" },
|
||||||
pixelStepCount: number;
|
stepCount: { type: "number" },
|
||||||
timestamp: number;
|
pixelCount: { type: "number" },
|
||||||
}
|
pixelStepCount: { type: "number" },
|
||||||
|
timestamp: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["userIds", "imageCount", "stepCount", "pixelCount", "pixelStepCount", "timestamp"],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
|
export type GlobalStats = jsonType<typeof globalStatsSchema>;
|
||||||
|
|
||||||
export const globalStats: GlobalStats = await getGlobalStats();
|
export const globalStats: GlobalStats = await getGlobalStats();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Memoizes the function result in KV storage.
|
||||||
|
*/
|
||||||
|
export function kvMemoize<A extends Deno.KvKey, R>(
|
||||||
|
db: Deno.Kv,
|
||||||
|
key: Deno.KvKey,
|
||||||
|
fn: (...args: A) => Promise<R>,
|
||||||
|
options?: {
|
||||||
|
expireIn?: number | ((result: R, ...args: A) => number);
|
||||||
|
shouldRecalculate?: (result: R, ...args: A) => boolean;
|
||||||
|
shouldCache?: (result: R, ...args: A) => boolean;
|
||||||
|
override?: {
|
||||||
|
set: (key: Deno.KvKey, args: A, value: R, options: { expireIn?: number }) => Promise<void>;
|
||||||
|
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
): (...args: A) => Promise<R> {
|
||||||
|
return async (...args) => {
|
||||||
|
const cachedResult = options?.override?.get
|
||||||
|
? await options.override.get(key, args)
|
||||||
|
: (await db.get<R>([...key, ...args])).value;
|
||||||
|
|
||||||
|
if (cachedResult != null) {
|
||||||
|
if (!options?.shouldRecalculate?.(cachedResult, ...args)) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fn(...args);
|
||||||
|
|
||||||
|
const expireIn = typeof options?.expireIn === "function"
|
||||||
|
? options.expireIn(result, ...args)
|
||||||
|
: options?.expireIn;
|
||||||
|
|
||||||
|
if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
|
||||||
|
if (options?.override?.set) {
|
||||||
|
await options.override.set(key, args, result, { expireIn });
|
||||||
|
} else {
|
||||||
|
await db.set([...key, ...args], result, { expireIn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ export async function processUploadQueue() {
|
||||||
// send caption in separate message if it couldn't fit
|
// send caption in separate message if it couldn't fit
|
||||||
if (caption.text.length > 1024 && caption.text.length <= 4096) {
|
if (caption.text.length > 1024 && caption.text.length <= 4096) {
|
||||||
await bot.api.sendMessage(state.chat.id, caption.text, {
|
await bot.api.sendMessage(state.chat.id, caption.text, {
|
||||||
reply_to_message_id: resultMessages[0]!.message_id,
|
reply_to_message_id: resultMessages[0].message_id,
|
||||||
allow_sending_without_reply: true,
|
allow_sending_without_reply: true,
|
||||||
entities: caption.entities,
|
entities: caption.entities,
|
||||||
maxWait: 60,
|
maxWait: 60,
|
||||||
|
@ -132,7 +132,7 @@ export async function processUploadQueue() {
|
||||||
// delete the status message
|
// delete the status message
|
||||||
await bot.api.deleteMessage(state.replyMessage.chat.id, state.replyMessage.message_id)
|
await bot.api.deleteMessage(state.replyMessage.chat.id, state.replyMessage.message_id)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}, { concurrency: 10 });
|
}, { concurrency: 3 });
|
||||||
|
|
||||||
uploadWorker.addEventListener("error", (e) => {
|
uploadWorker.addEventListener("error", (e) => {
|
||||||
error(`Upload failed for ${formatUserChat(e.detail.job.state)}: ${e.detail.error}`);
|
error(`Upload failed for ${formatUserChat(e.detail.job.state)}: ${e.detail.error}`);
|
||||||
|
|
|
@ -1,59 +1,24 @@
|
||||||
import { Static, t } from "elysia";
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { kvMemoize } from "../utils/kvMemoize.ts";
|
import { kvMemoize } from "./kvMemoize.ts";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
import { generationStore } from "./generationStore.ts";
|
import { generationStore } from "./generationStore.ts";
|
||||||
import { hoursToMilliseconds, isSameDay, minutesToMilliseconds } from "date-fns";
|
|
||||||
import { UTCDateMini } from "date-fns/utc";
|
|
||||||
|
|
||||||
export const userDailyStatsSchema = t.Object({
|
export const userDailyStatsSchema = {
|
||||||
imageCount: t.Number(),
|
type: "object",
|
||||||
pixelCount: t.Number(),
|
properties: {
|
||||||
pixelStepCount: t.Number(),
|
imageCount: { type: "number" },
|
||||||
timestamp: t.Number(),
|
pixelCount: { type: "number" },
|
||||||
});
|
timestamp: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["imageCount", "pixelCount", "timestamp"],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
export type UserDailyStats = Static<typeof userDailyStatsSchema>;
|
export type UserDailyStats = jsonType<typeof userDailyStatsSchema>;
|
||||||
|
|
||||||
export const getUserDailyStats = kvMemoize(
|
export const getUserDailyStats = kvMemoize(
|
||||||
db,
|
db,
|
||||||
["userDailyStats"],
|
["userDailyStats"],
|
||||||
async (userId: number, year: number, month: number, day: number): Promise<UserDailyStats> => {
|
async (userId: number, year: number, month: number, day: number): Promise<UserDailyStats> => {
|
||||||
let imageCount = 0;
|
throw new Error("Not implemented");
|
||||||
let pixelCount = 0;
|
|
||||||
let pixelStepCount = 0;
|
|
||||||
|
|
||||||
for await (
|
|
||||||
const generation of generationStore.listBy("fromId", {
|
|
||||||
after: new Date(Date.UTC(year, month - 1, day)),
|
|
||||||
before: new Date(Date.UTC(year, month - 1, day + 1)),
|
|
||||||
value: userId,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
imageCount++;
|
|
||||||
pixelCount += (generation.value.info?.width ?? 0) * (generation.value.info?.height ?? 0);
|
|
||||||
pixelStepCount += (generation.value.info?.width ?? 0) *
|
|
||||||
(generation.value.info?.height ?? 0) *
|
|
||||||
(generation.value.info?.steps ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageCount,
|
|
||||||
pixelCount,
|
|
||||||
pixelStepCount,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// expire in 1 minute if was calculated on the same day, otherwise 7-14 days.
|
|
||||||
expireIn: (result, year, month, day) => {
|
|
||||||
const requestDate = new UTCDateMini(year, month - 1, day);
|
|
||||||
const calculatedDate = new UTCDateMini(result.timestamp);
|
|
||||||
return isSameDay(requestDate, calculatedDate)
|
|
||||||
? minutesToMilliseconds(1)
|
|
||||||
: hoursToMilliseconds(24 * 7 + Math.random() * 24 * 7);
|
|
||||||
},
|
|
||||||
// should cache if the stats are non-zero
|
|
||||||
shouldCache: (result) =>
|
|
||||||
result.imageCount > 0 || result.pixelCount > 0 || result.pixelStepCount > 0,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
import { minutesToMilliseconds } from "date-fns";
|
import { minutesToMilliseconds } from "date-fns";
|
||||||
import { Store } from "indexed_kv";
|
import { Store } from "indexed_kv";
|
||||||
import { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
import { Static, t } from "elysia";
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
import { generationStore } from "./generationStore.ts";
|
import { generationStore } from "./generationStore.ts";
|
||||||
import { kvMemoize } from "../utils/kvMemoize.ts";
|
import { kvMemoize } from "./kvMemoize.ts";
|
||||||
import { sortBy } from "std/collections/sort_by.ts";
|
|
||||||
|
|
||||||
export const userStatsSchema = t.Object({
|
export const userStatsSchema = {
|
||||||
userId: t.Number(),
|
type: "object",
|
||||||
imageCount: t.Number(),
|
properties: {
|
||||||
stepCount: t.Number(),
|
userId: { type: "number" },
|
||||||
pixelCount: t.Number(),
|
imageCount: { type: "number" },
|
||||||
pixelStepCount: t.Number(),
|
stepCount: { type: "number" },
|
||||||
tagCountMap: t.Record(t.String(), t.Number()),
|
pixelCount: { type: "number" },
|
||||||
timestamp: t.Number(),
|
pixelStepCount: { type: "number" },
|
||||||
});
|
tagCountMap: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: { type: "number" },
|
||||||
|
},
|
||||||
|
timestamp: { type: "number" },
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
"userId",
|
||||||
|
"imageCount",
|
||||||
|
"stepCount",
|
||||||
|
"pixelCount",
|
||||||
|
"pixelStepCount",
|
||||||
|
"tagCountMap",
|
||||||
|
"timestamp",
|
||||||
|
],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
export type UserStats = Static<typeof userStatsSchema>;
|
export type UserStats = jsonType<typeof userStatsSchema>;
|
||||||
|
|
||||||
type UserStatsIndices = {
|
type UserStatsIndices = {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
@ -45,7 +59,7 @@ export const getUserStats = kvMemoize(
|
||||||
let stepCount = 0;
|
let stepCount = 0;
|
||||||
let pixelCount = 0;
|
let pixelCount = 0;
|
||||||
let pixelStepCount = 0;
|
let pixelStepCount = 0;
|
||||||
const tagCountMap = new Map<string, number>();
|
const tagCountMap: Record<string, number> = {};
|
||||||
|
|
||||||
info(`Calculating user stats for ${userId}`);
|
info(`Calculating user stats for ${userId}`);
|
||||||
|
|
||||||
|
@ -78,25 +92,17 @@ export const getUserStats = kvMemoize(
|
||||||
[];
|
[];
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const count = tagCountMap.get(tag) ?? 0;
|
tagCountMap[tag] = (tagCountMap[tag] ?? 0) + 1;
|
||||||
tagCountMap.set(tag, count + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagCountObj = Object.fromEntries(
|
|
||||||
sortBy(
|
|
||||||
Array.from(tagCountMap.entries()),
|
|
||||||
([_tag, count]) => -count,
|
|
||||||
).filter(([_tag, count]) => count >= 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
imageCount,
|
imageCount,
|
||||||
stepCount,
|
stepCount,
|
||||||
pixelCount,
|
pixelCount,
|
||||||
pixelStepCount,
|
pixelStepCount,
|
||||||
tagCountMap: tagCountObj,
|
tagCountMap,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
import { Store } from "indexed_kv";
|
import { Store } from "indexed_kv";
|
||||||
import { Static, t } from "elysia";
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
|
|
||||||
export const workerInstanceSchema = t.Object({
|
export const workerInstanceSchema = {
|
||||||
key: t.String(),
|
type: "object",
|
||||||
name: t.Nullable(t.String()),
|
properties: {
|
||||||
sdUrl: t.String(),
|
// used for counting stats
|
||||||
sdAuth: t.Nullable(t.Object({
|
key: { type: "string" },
|
||||||
user: t.String(),
|
// used for display
|
||||||
password: t.String(),
|
name: { type: ["string", "null"] },
|
||||||
})),
|
sdUrl: { type: "string" },
|
||||||
lastOnlineTime: t.Optional(t.Number()),
|
sdAuth: {
|
||||||
lastError: t.Optional(t.Object({
|
type: ["object", "null"],
|
||||||
message: t.String(),
|
properties: {
|
||||||
time: t.Number(),
|
user: { type: "string" },
|
||||||
})),
|
password: { type: "string" },
|
||||||
});
|
},
|
||||||
|
required: ["user", "password"],
|
||||||
|
},
|
||||||
|
lastOnlineTime: { type: "number" },
|
||||||
|
lastError: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
message: { type: "string" },
|
||||||
|
time: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["message", "time"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["key", "name", "sdUrl", "sdAuth"],
|
||||||
|
} as const satisfies JsonSchema;
|
||||||
|
|
||||||
export type WorkerInstance = Static<typeof workerInstanceSchema>;
|
export type WorkerInstance = jsonType<typeof workerInstanceSchema>;
|
||||||
|
|
||||||
export const workerInstanceStore = new Store<WorkerInstance>(db, "workerInstances", {
|
export const workerInstanceStore = new Store<WorkerInstance>(db, "workerInstances", {
|
||||||
indices: {},
|
indices: {},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { CommandContext } from "grammy";
|
||||||
import { bold, fmt, FormattedString } from "grammy_parse_mode";
|
import { bold, fmt, FormattedString } from "grammy_parse_mode";
|
||||||
import { distinctBy } from "std/collections/distinct_by.ts";
|
import { distinctBy } from "std/collections/distinct_by.ts";
|
||||||
import { error, info } from "std/log/mod.ts";
|
import { error, info } from "std/log/mod.ts";
|
||||||
import { adminStore } from "../app/adminStore.ts";
|
import { getConfig } from "../app/config.ts";
|
||||||
import { generationStore } from "../app/generationStore.ts";
|
import { generationStore } from "../app/generationStore.ts";
|
||||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||||
import { ErisContext } from "./mod.ts";
|
import { ErisContext } from "./mod.ts";
|
||||||
|
@ -12,9 +12,9 @@ export async function broadcastCommand(ctx: CommandContext<ErisContext>) {
|
||||||
return ctx.reply("I don't know who you are.");
|
return ctx.reply("I don't know who you are.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminEntry = await adminStore.get(["admins", ctx.from.id]);
|
const config = await getConfig();
|
||||||
|
|
||||||
if (!adminEntry.versionstamp) {
|
if (!config.adminUsernames.includes(ctx.from.username)) {
|
||||||
return ctx.reply("Only a bot admin can use this command.");
|
return ctx.reply("Only a bot admin can use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { generationQueue } from "../app/generationQueue.ts";
|
import { generationQueue } from "../app/generationQueue.ts";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { ErisContext } from "./mod.ts";
|
import { ErisContext } from "./mod.ts";
|
||||||
|
|
||||||
export async function cancelCommand(ctx: ErisContext) {
|
export async function cancelCommand(ctx: ErisContext) {
|
||||||
|
@ -8,11 +7,8 @@ export async function cancelCommand(ctx: ErisContext) {
|
||||||
.filter((job) => job.lockUntil < new Date())
|
.filter((job) => job.lockUntil < new Date())
|
||||||
.filter((j) => j.state.from.id === ctx.from?.id);
|
.filter((j) => j.state.from.id === ctx.from?.id);
|
||||||
for (const job of userJobs) await generationQueue.deleteJob(job.id);
|
for (const job of userJobs) await generationQueue.deleteJob(job.id);
|
||||||
await ctx.reply(
|
await ctx.reply(`Cancelled ${userJobs.length} jobs`, {
|
||||||
`Cancelled ${userJobs.length} jobs`,
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
allow_sending_without_reply: true,
|
allow_sending_without_reply: true,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { ErisContext } from "./mod.ts";
|
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { botDescription } from "./mod.ts"; // Import the description
|
|
||||||
|
|
||||||
export async function helpCommand(ctx: ErisContext) {
|
|
||||||
await ctx.reply(
|
|
||||||
botDescription, // Send the stored description
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
|
||||||
allow_sending_without_reply: true,
|
|
||||||
disable_web_page_preview: true, // Disable link previews
|
|
||||||
parse_mode: "Markdown"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -129,7 +129,7 @@ async function img2img(
|
||||||
chat: ctx.message.chat,
|
chat: ctx.message.chat,
|
||||||
requestMessage: ctx.message,
|
requestMessage: ctx.message,
|
||||||
replyMessage: replyMessage,
|
replyMessage: replyMessage,
|
||||||
}, { priority: 0, retryCount: 3, repeatDelayMs: 10_000 });
|
}, { retryCount: 3, repeatDelayMs: 10_000 });
|
||||||
|
|
||||||
debug(`Generation (img2img) enqueued for ${formatUserChat(ctx.message)}`);
|
debug(`Generation (img2img) enqueued for ${formatUserChat(ctx.message)}`);
|
||||||
}
|
}
|
||||||
|
|
160
bot/mod.ts
160
bot/mod.ts
|
@ -1,28 +1,17 @@
|
||||||
import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy";
|
import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy";
|
||||||
import { FileFlavor, hydrateFiles } from "grammy_files";
|
import { FileFlavor, hydrateFiles } from "grammy_files";
|
||||||
import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
|
import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
|
||||||
import { sequentialize } from "grammy_runner";
|
import { run, sequentialize } from "grammy_runner";
|
||||||
import { error, info, warning } from "std/log/mod.ts";
|
import { error, info, warning } from "std/log/mod.ts";
|
||||||
import { sessions } from "../api/sessionsRoute.ts";
|
import { sessions } from "../api/sessionsRoute.ts";
|
||||||
|
import { getConfig, setConfig } from "../app/config.ts";
|
||||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { broadcastCommand } from "./broadcastCommand.ts";
|
import { broadcastCommand } from "./broadcastCommand.ts";
|
||||||
import { cancelCommand } from "./cancelCommand.ts";
|
import { cancelCommand } from "./cancelCommand.ts";
|
||||||
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
|
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
|
||||||
import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts";
|
import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts";
|
||||||
import { queueCommand } from "./queueCommand.ts";
|
import { queueCommand } from "./queueCommand.ts";
|
||||||
import { txt2imgCommand, txt2imgQuestion } from "./txt2imgCommand.ts";
|
import { txt2imgCommand, txt2imgQuestion } from "./txt2imgCommand.ts";
|
||||||
import { helpCommand } from "./helpCommand.ts";
|
|
||||||
import { setConfig, getConfig } from "../app/config.ts";
|
|
||||||
|
|
||||||
// Set the new configuration
|
|
||||||
await setConfig({ maxUserJobs: 1, maxJobs: 500 });
|
|
||||||
|
|
||||||
// Fetch the updated configuration
|
|
||||||
const updatedConfig = await getConfig();
|
|
||||||
|
|
||||||
// Log the updated configuration to the console
|
|
||||||
console.log("Updated Configuration:", updatedConfig);
|
|
||||||
|
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
chat: ErisChatData;
|
chat: ErisChatData;
|
||||||
|
@ -30,11 +19,11 @@ interface SessionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErisChatData {
|
interface ErisChatData {
|
||||||
language?: string | undefined;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErisUserData {
|
interface ErisUserData {
|
||||||
params?: Record<string, string> | undefined;
|
params?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ErisContext =
|
export type ErisContext =
|
||||||
|
@ -105,110 +94,52 @@ bot.use(async (ctx, next) => {
|
||||||
await next();
|
await next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try {
|
try {
|
||||||
await ctx.reply(
|
await ctx.reply(`Handling update failed: ${err}`, {
|
||||||
`Handling update failed: ${err}`,
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
allow_sending_without_reply: true,
|
allow_sending_without_reply: true,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Declare the bot description variable
|
bot.api.setMyShortDescription("I can generate furry images from text");
|
||||||
export const botDescription = `I can generate furry images from text.
|
bot.api.setMyDescription(
|
||||||
\`/txt2img Nyx\`
|
"I can generate furry images from text. " +
|
||||||
|
"Send /txt2img to generate an image.",
|
||||||
If you want landscape or portrait:
|
);
|
||||||
\`/txt2img Nyx, Size: 576x832\`
|
bot.api.setMyCommands([
|
||||||
\`/txt2img Nyx, Size: 832x576\`
|
|
||||||
|
|
||||||
This bot uses the following model and will render nsfw and sfw:
|
|
||||||
\`EasyFluffV11.2.safetensors [821628644e]\`
|
|
||||||
|
|
||||||
There is no loRA support.
|
|
||||||
|
|
||||||
Please read about our terms of use:
|
|
||||||
https://nyx.akiru.de/disclaimer
|
|
||||||
|
|
||||||
You can support Nyx by sending her a coffee :3
|
|
||||||
ko-fi.com/nyxthebot`;
|
|
||||||
|
|
||||||
// Short description constant
|
|
||||||
const botShortDescription = `Generate furry images from text.
|
|
||||||
Use /help for more information.
|
|
||||||
https://ko-fi.com/nyxthebot
|
|
||||||
https://nyx.akiru.de`;
|
|
||||||
|
|
||||||
// Command descriptions
|
|
||||||
const botCommands = [
|
|
||||||
{ command: "txt2img", description: "Generate image from text" },
|
{ command: "txt2img", description: "Generate image from text" },
|
||||||
{ command: "img2img", description: "Generate image from image" },
|
{ command: "img2img", description: "Generate image from image" },
|
||||||
{ command: "pnginfo", description: "Try to extract prompt from raw file" },
|
{ command: "pnginfo", description: "Show generation parameters of an image" },
|
||||||
{ command: "queue", description: "Show the current queue" },
|
{ command: "queue", description: "Show the current queue" },
|
||||||
{ command: "cancel", description: "Cancel all your requests" },
|
{ command: "cancel", description: "Cancel all your requests" },
|
||||||
{ command: "help", description: "Show bot description" },
|
]);
|
||||||
];
|
|
||||||
|
|
||||||
// Wrap the calls in try-catch for error handling
|
|
||||||
async function setupBotCommands() {
|
|
||||||
try {
|
|
||||||
await bot.api.setMyShortDescription(botShortDescription);
|
|
||||||
} catch (err) {
|
|
||||||
error(`Failed to set short description: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await bot.api.setMyDescription(botDescription);
|
|
||||||
} catch (err) {
|
|
||||||
error(`Failed to set description: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await bot.api.setMyCommands(botCommands);
|
|
||||||
} catch (err) {
|
|
||||||
error(`Failed to set commands: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the setup function
|
|
||||||
setupBotCommands();
|
|
||||||
|
|
||||||
bot.command("start", async (ctx) => {
|
bot.command("start", async (ctx) => {
|
||||||
if (ctx.match) {
|
if (ctx.match) {
|
||||||
const id = ctx.match.trim();
|
const id = ctx.match.trim();
|
||||||
const session = sessions.get(id);
|
const session = sessions.get(id);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
await ctx.reply(
|
await ctx.reply("Login failed: Invalid session ID", {
|
||||||
"Login failed: Invalid session ID",
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.userId = ctx.from?.id;
|
session.userId = ctx.from?.id;
|
||||||
sessions.set(id, session);
|
sessions.set(id, session);
|
||||||
info(`User ${formatUserChat(ctx)} logged in`);
|
info(`User ${formatUserChat(ctx)} logged in`);
|
||||||
// TODO: show link to web ui
|
// TODO: show link to web ui
|
||||||
await ctx.reply(
|
await ctx.reply("Login successful! You can now return to the WebUI.", {
|
||||||
"Login successful! You can now return to the WebUI.",
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply("Hello! Use the /txt2img command to generate an image", {
|
||||||
"Hello! Use the /txt2img command to generate an image",
|
|
||||||
omitUndef({
|
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("txt2img", txt2imgCommand);
|
bot.command("txt2img", txt2imgCommand);
|
||||||
|
@ -224,34 +155,37 @@ bot.command("queue", queueCommand);
|
||||||
|
|
||||||
bot.command("cancel", cancelCommand);
|
bot.command("cancel", cancelCommand);
|
||||||
|
|
||||||
bot.command("help", helpCommand);
|
|
||||||
|
|
||||||
bot.command("broadcast", broadcastCommand);
|
bot.command("broadcast", broadcastCommand);
|
||||||
|
|
||||||
|
bot.command("pause", async (ctx) => {
|
||||||
|
if (!ctx.from?.username) return;
|
||||||
|
const config = await getConfig();
|
||||||
|
if (!config.adminUsernames.includes(ctx.from.username)) return;
|
||||||
|
if (config.pausedReason != null) {
|
||||||
|
return ctx.reply(`Already paused: ${config.pausedReason}`);
|
||||||
|
}
|
||||||
|
await setConfig({
|
||||||
|
pausedReason: ctx.match || "No reason given",
|
||||||
|
});
|
||||||
|
warning(`Bot paused by ${ctx.from.first_name} because ${config.pausedReason}`);
|
||||||
|
return ctx.reply("Paused");
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command("resume", async (ctx) => {
|
||||||
|
if (!ctx.from?.username) return;
|
||||||
|
const config = await getConfig();
|
||||||
|
if (!config.adminUsernames.includes(ctx.from.username)) return;
|
||||||
|
if (config.pausedReason == null) return ctx.reply("Already running");
|
||||||
|
await setConfig({ pausedReason: null });
|
||||||
|
info(`Bot resumed by ${ctx.from.first_name}`);
|
||||||
|
return ctx.reply("Resumed");
|
||||||
|
});
|
||||||
|
|
||||||
bot.command("crash", () => {
|
bot.command("crash", () => {
|
||||||
throw new Error("Crash command used");
|
throw new Error("Crash command used");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up the webhook in the telegram API and initialize the bot
|
export async function runBot() {
|
||||||
await bot.api.setWebhook('https://nyx.akiru.de/webhook');
|
const runner = run(bot);
|
||||||
await bot.init();
|
await runner.task();
|
||||||
|
|
||||||
// Function to handle incoming webhook requests
|
|
||||||
export async function handleWebhook(req: Request): Promise<Response> {
|
|
||||||
try {
|
|
||||||
const body = await req.json();
|
|
||||||
// console.log("Received webhook data:", JSON.stringify(body, null, 2));
|
|
||||||
|
|
||||||
// Log before processing update
|
|
||||||
// console.log("Processing update through handleUpdate...");
|
|
||||||
await bot.handleUpdate(body); // Process the update
|
|
||||||
// Log after processing update
|
|
||||||
// console.log("Update processed successfully.");
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } });
|
|
||||||
} catch (error) {
|
|
||||||
// Detailed error logging
|
|
||||||
console.error("Error in handleWebhook:", error);
|
|
||||||
return new Response("Error", { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
import * as ExifReader from "exifreader";
|
import { decode } from "png_chunk_text";
|
||||||
|
import extractChunks from "png_chunks_extract";
|
||||||
|
|
||||||
export function getPngInfo(pngData: ArrayBuffer): string | undefined {
|
export function getPngInfo(pngData: Uint8Array): string | undefined {
|
||||||
const info = ExifReader.load(pngData);
|
return extractChunks(pngData)
|
||||||
|
.filter((chunk) => chunk.name === "tEXt" || chunk.name === "iTXt")
|
||||||
if (info.UserComment?.value && Array.isArray(info.UserComment.value)) {
|
.map((chunk) => decode(chunk.data))
|
||||||
// JPEG image
|
.find((textChunk) => textChunk.keyword === "parameters")
|
||||||
return String.fromCharCode(
|
?.text;
|
||||||
...info.UserComment.value
|
|
||||||
.filter((char): char is number => typeof char == "number")
|
|
||||||
.filter((char) => char !== 0),
|
|
||||||
).replace("UNICODE", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.parameters?.description) {
|
|
||||||
// PNG image
|
|
||||||
return info.parameters.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown image type
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PngInfo {
|
export interface PngInfo {
|
||||||
|
@ -50,7 +38,7 @@ export function parsePngInfo(
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u);
|
const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u);
|
||||||
if (paramValuePair) {
|
if (paramValuePair) {
|
||||||
const [_match, param = "", value = ""] = paramValuePair;
|
const [, param, value] = paramValuePair;
|
||||||
switch (param.replace(/\s+/u, "").toLowerCase()) {
|
switch (param.replace(/\s+/u, "").toLowerCase()) {
|
||||||
case "positiveprompt":
|
case "positiveprompt":
|
||||||
case "positive":
|
case "positive":
|
||||||
|
@ -83,7 +71,7 @@ export function parsePngInfo(
|
||||||
case "size":
|
case "size":
|
||||||
case "resolution": {
|
case "resolution": {
|
||||||
part = "params";
|
part = "params";
|
||||||
const [width = 0, height = 0] = value.trim()
|
const [width, height] = value.trim()
|
||||||
.split(/\s*[x,]\s*/u, 2)
|
.split(/\s*[x,]\s*/u, 2)
|
||||||
.map((v) => v.trim())
|
.map((v) => v.trim())
|
||||||
.map(Number);
|
.map(Number);
|
||||||
|
@ -119,10 +107,8 @@ export function parsePngInfo(
|
||||||
part = "params";
|
part = "params";
|
||||||
if (shouldParseSeed) {
|
if (shouldParseSeed) {
|
||||||
const seed = Number(value.trim());
|
const seed = Number(value.trim());
|
||||||
if (Number.isFinite(seed)) {
|
|
||||||
params.seed = seed;
|
params.seed = seed;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "model":
|
case "model":
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { CommandContext } from "grammy";
|
import { CommandContext } from "grammy";
|
||||||
import { bold, fmt } from "grammy_parse_mode";
|
import { bold, fmt } from "grammy_parse_mode";
|
||||||
import { StatelessQuestion } from "grammy_stateless_question";
|
import { StatelessQuestion } from "grammy_stateless_question";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { ErisContext } from "./mod.ts";
|
import { ErisContext } from "./mod.ts";
|
||||||
import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts";
|
import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts";
|
||||||
|
|
||||||
|
@ -20,31 +19,29 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
||||||
const document = ctx.message?.document ||
|
const document = ctx.message?.document ||
|
||||||
(includeRepliedTo ? ctx.message?.reply_to_message?.document : undefined);
|
(includeRepliedTo ? ctx.message?.reply_to_message?.document : undefined);
|
||||||
|
|
||||||
if (document?.mime_type !== "image/png" && document?.mime_type !== "image/jpeg") {
|
if (document?.mime_type !== "image/png") {
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Please send me a PNG or JPEG file." +
|
"Please send me a PNG file." +
|
||||||
pnginfoQuestion.messageSuffixMarkdown(),
|
pnginfoQuestion.messageSuffixMarkdown(),
|
||||||
omitUndef(
|
|
||||||
{
|
{
|
||||||
reply_markup: { force_reply: true, selective: true },
|
reply_markup: { force_reply: true, selective: true },
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
} as const,
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await ctx.api.getFile(document.file_id);
|
const file = await ctx.api.getFile(document.file_id);
|
||||||
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
||||||
const info = getPngInfo(buffer);
|
const params = parsePngInfo(getPngInfo(new Uint8Array(buffer)) ?? "");
|
||||||
if (!info) {
|
|
||||||
return void await ctx.reply(
|
if (!params.prompt) {
|
||||||
"No info found in file.",
|
ctx.reply("File doesn't contain any parameters.", {
|
||||||
omitUndef({ reply_to_message_id: ctx.message?.message_id }),
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
);
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const params = parsePngInfo(info, undefined, true);
|
|
||||||
|
|
||||||
const paramsText = fmt([
|
const paramsText = fmt([
|
||||||
`${params.prompt}\n`,
|
`${params.prompt}\n`,
|
||||||
|
@ -56,11 +53,8 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
||||||
params.width && params.height ? fmt`${bold("Size")}: ${params.width}x${params.height}` : "",
|
params.width && params.height ? fmt`${bold("Size")}: ${params.width}x${params.height}` : "",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(paramsText.text, {
|
||||||
paramsText.text,
|
|
||||||
omitUndef({
|
|
||||||
entities: paramsText.entities,
|
entities: paramsText.entities,
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
import { CommandContext } from "grammy";
|
import { CommandContext } from "grammy";
|
||||||
import { bold, fmt } from "grammy_parse_mode";
|
import { bold, fmt } from "grammy_parse_mode";
|
||||||
import { activeGenerationWorkers, generationQueue } from "../app/generationQueue.ts";
|
import { activeGenerationWorkers, generationQueue } from "../app/generationQueue.ts";
|
||||||
import { workerInstanceStore } from "../app/workerInstanceStore.ts";
|
|
||||||
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
|
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
import { ErisContext } from "./mod.ts";
|
import { ErisContext } from "./mod.ts";
|
||||||
|
import { workerInstanceStore } from "../app/workerInstanceStore.ts";
|
||||||
|
|
||||||
export async function queueCommand(ctx: CommandContext<ErisContext>) {
|
export async function queueCommand(ctx: CommandContext<ErisContext>) {
|
||||||
let formattedMessage = await getMessageText();
|
let formattedMessage = await getMessageText();
|
||||||
const queueMessage = await ctx.replyFmt(
|
const queueMessage = await ctx.replyFmt(formattedMessage, {
|
||||||
formattedMessage,
|
|
||||||
omitUndef({
|
|
||||||
disable_notification: true,
|
disable_notification: true,
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
handleFutureUpdates().catch(() => undefined);
|
handleFutureUpdates().catch(() => undefined);
|
||||||
|
|
||||||
async function getMessageText() {
|
async function getMessageText() {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { generationQueue } from "../app/generationQueue.ts";
|
||||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||||
import { ErisContext } from "./mod.ts";
|
import { ErisContext } from "./mod.ts";
|
||||||
import { getPngInfo, parsePngInfo, PngInfo } from "./parsePngInfo.ts";
|
import { getPngInfo, parsePngInfo, PngInfo } from "./parsePngInfo.ts";
|
||||||
import { adminStore } from "../app/adminStore.ts";
|
|
||||||
|
|
||||||
export const txt2imgQuestion = new StatelessQuestion<ErisContext>(
|
export const txt2imgQuestion = new StatelessQuestion<ErisContext>(
|
||||||
"txt2img",
|
"txt2img",
|
||||||
|
@ -30,7 +29,6 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
let priority = 0;
|
|
||||||
|
|
||||||
if (config.pausedReason != null) {
|
if (config.pausedReason != null) {
|
||||||
await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`, {
|
await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`, {
|
||||||
|
@ -39,11 +37,6 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminEntry = await adminStore.get(["admins", ctx.from.id]);
|
|
||||||
|
|
||||||
if (adminEntry.versionstamp) {
|
|
||||||
priority = 1;
|
|
||||||
} else {
|
|
||||||
const jobs = await generationQueue.getAllJobs();
|
const jobs = await generationQueue.getAllJobs();
|
||||||
if (jobs.length >= config.maxJobs) {
|
if (jobs.length >= config.maxJobs) {
|
||||||
await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
|
await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
|
||||||
|
@ -54,12 +47,11 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
||||||
|
|
||||||
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
|
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
|
||||||
if (userJobs.length >= config.maxUserJobs) {
|
if (userJobs.length >= config.maxUserJobs) {
|
||||||
await ctx.reply(`You already have ${userJobs.length} jobs in the queue. Try again later.`, {
|
await ctx.reply(`You already have ${userJobs.length} jobs in queue. Try again later.`, {
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let params: Partial<PngInfo> = {};
|
let params: Partial<PngInfo> = {};
|
||||||
|
|
||||||
|
@ -68,26 +60,17 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
||||||
if (includeRepliedTo && repliedToMsg?.document?.mime_type === "image/png") {
|
if (includeRepliedTo && repliedToMsg?.document?.mime_type === "image/png") {
|
||||||
const file = await ctx.api.getFile(repliedToMsg.document.file_id);
|
const file = await ctx.api.getFile(repliedToMsg.document.file_id);
|
||||||
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
||||||
params = parsePngInfo(getPngInfo(buffer) ?? "", params);
|
params = parsePngInfo(getPngInfo(new Uint8Array(buffer)) ?? "", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliedToText = repliedToMsg?.text || repliedToMsg?.caption;
|
const repliedToText = repliedToMsg?.text || repliedToMsg?.caption;
|
||||||
const isReply = includeRepliedTo && repliedToText;
|
if (includeRepliedTo && repliedToText) {
|
||||||
|
|
||||||
if (isReply) {
|
|
||||||
// TODO: remove bot command from replied to text
|
// TODO: remove bot command from replied to text
|
||||||
params = parsePngInfo(repliedToText, params);
|
params = parsePngInfo(repliedToText, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
params = parsePngInfo(match, params, true);
|
params = parsePngInfo(match, params, true);
|
||||||
|
|
||||||
if (isReply) {
|
|
||||||
const parsedInfo = parsePngInfo(repliedToText, undefined, true);
|
|
||||||
if (parsedInfo.prompt !== params.prompt) {
|
|
||||||
params.seed = parsedInfo.seed ?? -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.prompt) {
|
if (!params.prompt) {
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Please tell me what you want to see." +
|
"Please tell me what you want to see." +
|
||||||
|
@ -111,7 +94,7 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
||||||
chat: ctx.message.chat,
|
chat: ctx.message.chat,
|
||||||
requestMessage: ctx.message,
|
requestMessage: ctx.message,
|
||||||
replyMessage: replyMessage,
|
replyMessage: replyMessage,
|
||||||
}, { retryCount: 3, retryDelayMs: 10_000, priority: priority });
|
}, { retryCount: 3, retryDelayMs: 10_000 });
|
||||||
|
|
||||||
debug(`Generation (txt2img) enqueued for ${formatUserChat(ctx.message)}`);
|
debug(`Generation (txt2img) enqueued for ${formatUserChat(ctx.message)}`);
|
||||||
}
|
}
|
||||||
|
|
57
deno.json
57
deno.json
|
@ -1,37 +1,32 @@
|
||||||
{
|
{
|
||||||
|
"tasks": {
|
||||||
|
"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": {
|
||||||
"exactOptionalPropertyTypes": true,
|
"jsx": "react"
|
||||||
"jsx": "react",
|
|
||||||
"noUncheckedIndexedAccess": true
|
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"lineWidth": 100
|
"lineWidth": 100
|
||||||
},
|
},
|
||||||
"imports": {
|
"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",
|
"async": "https://deno.land/x/async@v2.0.2/mod.ts",
|
||||||
"date-fns": "https://cdn.skypack.dev/date-fns@2.30.0?dts",
|
"date-fns": "https://cdn.skypack.dev/date-fns@2.30.0?dts",
|
||||||
"date-fns/utc": "https://cdn.skypack.dev/@date-fns/utc@1.1.0?dts",
|
|
||||||
"elysia": "https://esm.sh/elysia@0.7.21?dev",
|
|
||||||
"elysia/eden": "https://esm.sh/@elysiajs/eden@0.7.4?external=elysia&dev",
|
|
||||||
"elysia/swagger": "https://esm.sh/@elysiajs/swagger@0.7.4?external=elysia&dev",
|
|
||||||
"exifreader": "https://esm.sh/exifreader@4.14.1",
|
|
||||||
"file_type": "https://esm.sh/file-type@18.5.0",
|
"file_type": "https://esm.sh/file-type@18.5.0",
|
||||||
"grammy": "https://lib.deno.dev/x/grammy@1/mod.ts",
|
|
||||||
"grammy_autoquote": "https://lib.deno.dev/x/grammy_autoquote@1/mod.ts",
|
"grammy_autoquote": "https://lib.deno.dev/x/grammy_autoquote@1/mod.ts",
|
||||||
"grammy_files": "https://lib.deno.dev/x/grammy_files@1/mod.ts",
|
"grammy_files": "https://lib.deno.dev/x/grammy_files@1/mod.ts",
|
||||||
"grammy_parse_mode": "https://lib.deno.dev/x/grammy_parse_mode@1/mod.ts",
|
"grammy_parse_mode": "https://lib.deno.dev/x/grammy_parse_mode@1/mod.ts",
|
||||||
"grammy_runner": "https://lib.deno.dev/x/grammy_runner@2/mod.ts",
|
"grammy_runner": "https://lib.deno.dev/x/grammy_runner@2/mod.ts",
|
||||||
"grammy_stateless_question": "https://lib.deno.dev/x/grammy_stateless_question_alpha@3/mod.ts",
|
"grammy_stateless_question": "https://lib.deno.dev/x/grammy_stateless_question_alpha@3/mod.ts",
|
||||||
"grammy_types": "https://lib.deno.dev/x/grammy_types@3/mod.ts",
|
"grammy_types": "https://lib.deno.dev/x/grammy_types@3/mod.ts",
|
||||||
"indexed_kv": "https://deno.land/x/indexed_kv@v0.6.1/mod.ts",
|
"grammy": "https://lib.deno.dev/x/grammy@1/mod.ts",
|
||||||
|
"indexed_kv": "https://deno.land/x/indexed_kv@v0.5.0/mod.ts",
|
||||||
"kvfs": "https://deno.land/x/kvfs@v0.1.0/mod.ts",
|
"kvfs": "https://deno.land/x/kvfs@v0.1.0/mod.ts",
|
||||||
"kvmq": "https://deno.land/x/kvmq@v0.3.0/mod.ts",
|
"kvmq": "https://deno.land/x/kvmq@v0.3.0/mod.ts",
|
||||||
"openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6",
|
"openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6",
|
||||||
"react": "https://esm.sh/react@18.2.0?dev",
|
"png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0",
|
||||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?external=react&dev",
|
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
|
||||||
"react-flip-move": "https://esm.sh/react-flip-move@3.0.5?external=react&dev",
|
|
||||||
"react-intl": "https://esm.sh/react-intl@6.4.7?external=react&dev",
|
|
||||||
"react-router-dom": "https://esm.sh/react-router-dom@6.16.0?external=react&dev",
|
|
||||||
"reroute": "https://deno.land/x/reroute@v0.1.0/mod.ts",
|
"reroute": "https://deno.land/x/reroute@v0.1.0/mod.ts",
|
||||||
"serve_spa": "https://deno.land/x/serve_spa@v0.2.0/mod.ts",
|
"serve_spa": "https://deno.land/x/serve_spa@v0.2.0/mod.ts",
|
||||||
"std/async/": "https://deno.land/std@0.201.0/async/",
|
"std/async/": "https://deno.land/std@0.201.0/async/",
|
||||||
|
@ -41,23 +36,19 @@
|
||||||
"std/fmt/": "https://deno.land/std@0.202.0/fmt/",
|
"std/fmt/": "https://deno.land/std@0.202.0/fmt/",
|
||||||
"std/log/": "https://deno.land/std@0.201.0/log/",
|
"std/log/": "https://deno.land/std@0.201.0/log/",
|
||||||
"std/path/": "https://deno.land/std@0.204.0/path/",
|
"std/path/": "https://deno.land/std@0.204.0/path/",
|
||||||
"swr": "https://esm.sh/swr@2.2.4?external=react&dev",
|
"t_rest/server": "https://esm.sh/ty-rest@0.4.0/server?dev",
|
||||||
"swr/mutation": "https://esm.sh/swr@2.2.4/mutation?external=react&dev",
|
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts",
|
||||||
"twind/core": "https://esm.sh/@twind/core@1.1.3",
|
|
||||||
"twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4",
|
"@twind/core": "https://esm.sh/@twind/core@1.1.3?dev",
|
||||||
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts"
|
"@twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4?dev",
|
||||||
},
|
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
||||||
"lint": {
|
"react-flip-move": "https://esm.sh/react-flip-move@3.0.5?dev",
|
||||||
"rules": {
|
"react-intl": "https://esm.sh/react-intl@6.4.7?external=react&alias=@types/react:react&dev",
|
||||||
"exclude": [
|
"react-router-dom": "https://esm.sh/react-router-dom@6.16.0?dev",
|
||||||
"require-await"
|
"react": "https://esm.sh/react@18.2.0?dev",
|
||||||
]
|
"swr": "https://esm.sh/swr@2.2.4?dev",
|
||||||
}
|
"swr/mutation": "https://esm.sh/swr@2.2.4/mutation?dev",
|
||||||
},
|
"t_rest/client": "https://esm.sh/ty-rest@0.4.0/client?dev",
|
||||||
"tasks": {
|
"use-local-storage": "https://esm.sh/use-local-storage@3.0.0?dev"
|
||||||
"check": "deno check --unstable main.ts && deno check --unstable ui/main.tsx",
|
|
||||||
"generate": "deno run npm:openapi-typescript http://localhost:7861/openapi.json -o app/sdApi.ts",
|
|
||||||
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts",
|
|
||||||
"test": "deno test --unstable"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
179
deno.lock
179
deno.lock
|
@ -2,33 +2,33 @@
|
||||||
"version": "3",
|
"version": "3",
|
||||||
"packages": {
|
"packages": {
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"npm:@types/node": "npm:@types/node@18.16.19"
|
"npm:telegram-format@2": "npm:telegram-format@2.1.0"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"@types/node@18.16.19": {
|
"telegram-format@2.1.0": {
|
||||||
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
|
"integrity": "sha512-V2thkhKzcNVL26h/ANeat/Z+AXBaDGoizs4cab6Kpq/w+d+Ai6M/AFFkuBwvCkZXqb+7UW2vt3Dko5+kKcOIHg==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirects": {
|
"redirects": {
|
||||||
"https://cdn.skypack.dev/-/@date-fns/utc@v1.1.0-EKCmNY9452DyFBJfVVmZ/dist=es2019,mode=types/date.d.ts": "https://cdn.skypack.dev/-/@date-fns/utc@v1.1.0-EKCmNY9452DyFBJfVVmZ/dist=es2019,mode=types/date/index.d.ts",
|
"https://cdn.skypack.dev/-/@date-fns/utc@v1.1.0-EKCmNY9452DyFBJfVVmZ/dist=es2019,mode=types/date.d.ts": "https://cdn.skypack.dev/-/@date-fns/utc@v1.1.0-EKCmNY9452DyFBJfVVmZ/dist=es2019,mode=types/date/index.d.ts",
|
||||||
|
"https://deno.land/std/path/mod.ts": "https://deno.land/std@0.204.0/path/mod.ts",
|
||||||
"https://esm.sh/@grammyjs/types@2": "https://esm.sh/@grammyjs/types@2.12.1",
|
"https://esm.sh/@grammyjs/types@2": "https://esm.sh/@grammyjs/types@2.12.1",
|
||||||
"https://esm.sh/@grammyjs/types@v2": "https://esm.sh/@grammyjs/types@2.0.0",
|
"https://esm.sh/@grammyjs/types@v2": "https://esm.sh/@grammyjs/types@2.0.0",
|
||||||
"https://esm.sh/@swc/core@1.2.212/types.d.ts": "https://esm.sh/v133/@swc/core@1.2.212/types.d.ts",
|
"https://esm.sh/@swc/core@1.2.212/types.d.ts": "https://esm.sh/v133/@swc/core@1.2.212/types.d.ts",
|
||||||
"https://esm.sh/telegram-format@2": "https://esm.sh/telegram-format@2.1.0",
|
|
||||||
"https://esm.sh/v128/@types/react@~18.2/index.d.ts": "https://esm.sh/v128/@types/react@18.2.25/index.d.ts",
|
"https://esm.sh/v128/@types/react@~18.2/index.d.ts": "https://esm.sh/v128/@types/react@18.2.25/index.d.ts",
|
||||||
"https://esm.sh/v133/@types/png-chunk-text@~1.0/index.d.ts": "https://esm.sh/v133/@types/png-chunk-text@1.0.2/index.d.ts",
|
"https://esm.sh/v133/@types/png-chunk-text@~1.0/index.d.ts": "https://esm.sh/v133/@types/png-chunk-text@1.0.1/index.d.ts",
|
||||||
"https://esm.sh/v133/@types/png-chunks-extract@~1.0/index.d.ts": "https://esm.sh/v133/@types/png-chunks-extract@1.0.1/index.d.ts",
|
"https://esm.sh/v133/@types/png-chunks-extract@~1.0/index.d.ts": "https://esm.sh/v133/@types/png-chunks-extract@1.0.0/index.d.ts",
|
||||||
"https://esm.sh/v133/@types/react-dom@~18.2/X-ZS9yZWFjdA/client~.d.ts": "https://esm.sh/v133/@types/react-dom@18.2.14/X-ZS9yZWFjdA/client~.d.ts",
|
"https://esm.sh/v133/@types/react-dom@~18.2/client~.d.ts": "https://esm.sh/v133/@types/react-dom@18.2.13/client~.d.ts",
|
||||||
"https://lib.deno.dev/x/grammy@1/mod.ts": "https://deno.land/x/grammy@v1.19.2/mod.ts",
|
"https://lib.deno.dev/x/grammy@1/mod.ts": "https://deno.land/x/grammy@v1.19.1/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy@^1.0/mod.ts": "https://deno.land/x/grammy@v1.19.2/mod.ts",
|
"https://lib.deno.dev/x/grammy@^1.0/mod.ts": "https://deno.land/x/grammy@v1.19.1/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy@v1/mod.ts": "https://deno.land/x/grammy@v1.19.2/mod.ts",
|
"https://lib.deno.dev/x/grammy@v1/mod.ts": "https://deno.land/x/grammy@v1.19.1/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy@v1/types.ts": "https://deno.land/x/grammy@v1.19.2/types.ts",
|
"https://lib.deno.dev/x/grammy@v1/types.ts": "https://deno.land/x/grammy@v1.19.1/types.ts",
|
||||||
"https://lib.deno.dev/x/grammy_files@1/mod.ts": "https://deno.land/x/grammy_files@v1.0.4/mod.ts",
|
"https://lib.deno.dev/x/grammy_files@1/mod.ts": "https://deno.land/x/grammy_files@v1.0.4/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy_parse_mode@1/mod.ts": "https://deno.land/x/grammy_parse_mode@1.8.1/mod.ts",
|
"https://lib.deno.dev/x/grammy_parse_mode@1/mod.ts": "https://deno.land/x/grammy_parse_mode@1.8.1/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy_runner@2/mod.ts": "https://deno.land/x/grammy_runner@v2.0.3/mod.ts",
|
"https://lib.deno.dev/x/grammy_runner@2/mod.ts": "https://deno.land/x/grammy_runner@v2.0.3/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy_stateless_question_alpha@3/mod.ts": "https://deno.land/x/grammy_stateless_question_alpha@v3.0.5/mod.ts",
|
"https://lib.deno.dev/x/grammy_stateless_question_alpha@3/mod.ts": "https://deno.land/x/grammy_stateless_question_alpha@v3.0.4/mod.ts",
|
||||||
"https://lib.deno.dev/x/grammy_types@3/mod.ts": "https://deno.land/x/grammy_types@v3.3.0/mod.ts"
|
"https://lib.deno.dev/x/grammy_types@3/mod.ts": "https://deno.land/x/grammy_types@v3.3.0/mod.ts"
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
@ -106,7 +106,6 @@
|
||||||
"https://deno.land/std@0.202.0/collections/deep_merge.ts": "9db788ba56cb05b65c77166b789e58e125dff159b7f41bf4d19dc1cba19ecb8b",
|
"https://deno.land/std@0.202.0/collections/deep_merge.ts": "9db788ba56cb05b65c77166b789e58e125dff159b7f41bf4d19dc1cba19ecb8b",
|
||||||
"https://deno.land/std@0.202.0/collections/distinct_by.ts": "3afe11d81eafb30c7c9dbf568d94357f3d88153292c00671b72cd695deae6602",
|
"https://deno.land/std@0.202.0/collections/distinct_by.ts": "3afe11d81eafb30c7c9dbf568d94357f3d88153292c00671b72cd695deae6602",
|
||||||
"https://deno.land/std@0.202.0/collections/max_by.ts": "9d5940986aac51b2e4feaebef9cd8bf6e1eceeee5edcf3303e334b737f99520d",
|
"https://deno.land/std@0.202.0/collections/max_by.ts": "9d5940986aac51b2e4feaebef9cd8bf6e1eceeee5edcf3303e334b737f99520d",
|
||||||
"https://deno.land/std@0.202.0/collections/sort_by.ts": "1207755af756a5da04bebff39146c93cbe54f7870a5d67cf6922e871d96a01d5",
|
|
||||||
"https://deno.land/std@0.202.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d",
|
"https://deno.land/std@0.202.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d",
|
||||||
"https://deno.land/std@0.202.0/flags/mod.ts": "0948466fc437f017f00c0b972a422b3dc3317a790bcf326429d23182977eaf9f",
|
"https://deno.land/std@0.202.0/flags/mod.ts": "0948466fc437f017f00c0b972a422b3dc3317a790bcf326429d23182977eaf9f",
|
||||||
"https://deno.land/std@0.202.0/fmt/bytes.ts": "f29cf69e0791d375f9f5d94ae1f0641e5a03b975f32ddf86d70f70fdf37e7b6a",
|
"https://deno.land/std@0.202.0/fmt/bytes.ts": "f29cf69e0791d375f9f5d94ae1f0641e5a03b975f32ddf86d70f70fdf37e7b6a",
|
||||||
|
@ -236,25 +235,25 @@
|
||||||
"https://deno.land/x/async@v2.0.2/stack.ts": "e24ebcbdcab032783e6b278b938475c221fbfb86f8eb71d3752679fcdf132d42",
|
"https://deno.land/x/async@v2.0.2/stack.ts": "e24ebcbdcab032783e6b278b938475c221fbfb86f8eb71d3752679fcdf132d42",
|
||||||
"https://deno.land/x/async@v2.0.2/state.ts": "a71692b72371239120b196d0c6a249631bab867dd499e85c422c2e5ec9695999",
|
"https://deno.land/x/async@v2.0.2/state.ts": "a71692b72371239120b196d0c6a249631bab867dd499e85c422c2e5ec9695999",
|
||||||
"https://deno.land/x/async@v2.0.2/testutil.ts": "c4b4092066ad6f24cf84012781831ff188e656a1e81abf31b0f712d2e1ad07b7",
|
"https://deno.land/x/async@v2.0.2/testutil.ts": "c4b4092066ad6f24cf84012781831ff188e656a1e81abf31b0f712d2e1ad07b7",
|
||||||
"https://deno.land/x/grammy@v1.19.2/bot.ts": "8d13cd72f1512e3f76d685131c7d0db5ba51f2c877db5ac2c0aa4b0f6f876aa8",
|
"https://deno.land/x/grammy@v1.19.1/bot.ts": "ff38517817fdc104ed2ef0ab210d5ba1f67675510eabe33b74abd1e586b91316",
|
||||||
"https://deno.land/x/grammy@v1.19.2/composer.ts": "8660f86990f4ef2afc4854a1f2610bb8d60f88116f3a57c8e5515a77b277f82d",
|
"https://deno.land/x/grammy@v1.19.1/composer.ts": "8660f86990f4ef2afc4854a1f2610bb8d60f88116f3a57c8e5515a77b277f82d",
|
||||||
"https://deno.land/x/grammy@v1.19.2/context.ts": "4cf51ed7538750edb4379f757f6b8b3c1f3987242d58393160b463c9ca13c997",
|
"https://deno.land/x/grammy@v1.19.1/context.ts": "4cf51ed7538750edb4379f757f6b8b3c1f3987242d58393160b463c9ca13c997",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/constants.ts": "3be0f6393ab2b2995fad6bcd4c9cf8a1a615ae4543fc864c107ba0dd38f123f6",
|
"https://deno.land/x/grammy@v1.19.1/convenience/constants.ts": "3be0f6393ab2b2995fad6bcd4c9cf8a1a615ae4543fc864c107ba0dd38f123f6",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/frameworks.ts": "77e2f9fc841ab92d4310b556126447a42f131ad976a6adfff454c016f339b28e",
|
"https://deno.land/x/grammy@v1.19.1/convenience/frameworks.ts": "4d4e5ecdcb4f48d3b317c35d8201800c45002e1e195af1b5d7609617f4bdc656",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/inline_query.ts": "409d1940c7670708064efa495003bcbfdf6763a756b2e6303c464489fd3394ff",
|
"https://deno.land/x/grammy@v1.19.1/convenience/inline_query.ts": "409d1940c7670708064efa495003bcbfdf6763a756b2e6303c464489fd3394ff",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/input_media.ts": "7af72a5fdb1af0417e31b1327003f536ddfdf64e06ab8bc7f5da6b574de38658",
|
"https://deno.land/x/grammy@v1.19.1/convenience/input_media.ts": "7af72a5fdb1af0417e31b1327003f536ddfdf64e06ab8bc7f5da6b574de38658",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/keyboard.ts": "21220dc2321c40203c699fa4eb7b07ed8217956ea0477c241a551224a58a278d",
|
"https://deno.land/x/grammy@v1.19.1/convenience/keyboard.ts": "21220dc2321c40203c699fa4eb7b07ed8217956ea0477c241a551224a58a278d",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/session.ts": "f92d57b6b2b61920912cf5c44d4db2f6ca999fe4f9adef170c321889d49667c2",
|
"https://deno.land/x/grammy@v1.19.1/convenience/session.ts": "f92d57b6b2b61920912cf5c44d4db2f6ca999fe4f9adef170c321889d49667c2",
|
||||||
"https://deno.land/x/grammy@v1.19.2/convenience/webhook.ts": "f1da7d6426171fb7b5d5f6b59633f91d3bab9a474eea821f714932650965eb9e",
|
"https://deno.land/x/grammy@v1.19.1/convenience/webhook.ts": "f1da7d6426171fb7b5d5f6b59633f91d3bab9a474eea821f714932650965eb9e",
|
||||||
"https://deno.land/x/grammy@v1.19.2/core/api.ts": "7d4d8df3567e322ab3b793360ee48da09f46ad531ef994a87b3e6aef4ec23bf2",
|
"https://deno.land/x/grammy@v1.19.1/core/api.ts": "7d4d8df3567e322ab3b793360ee48da09f46ad531ef994a87b3e6aef4ec23bf2",
|
||||||
"https://deno.land/x/grammy@v1.19.2/core/client.ts": "39639e4f5fc3a3f9d528c6906d7e3cdc268cf5d33929eeab801bb39642a59103",
|
"https://deno.land/x/grammy@v1.19.1/core/client.ts": "39639e4f5fc3a3f9d528c6906d7e3cdc268cf5d33929eeab801bb39642a59103",
|
||||||
"https://deno.land/x/grammy@v1.19.2/core/error.ts": "4638b2127ebe60249c78b83011d468f5e1e1a87748d32fe11a8200d9f824ad13",
|
"https://deno.land/x/grammy@v1.19.1/core/error.ts": "4638b2127ebe60249c78b83011d468f5e1e1a87748d32fe11a8200d9f824ad13",
|
||||||
"https://deno.land/x/grammy@v1.19.2/core/payload.ts": "420e17c3c2830b5576ea187cfce77578fe09f1204b25c25ea2f220ca7c86e73b",
|
"https://deno.land/x/grammy@v1.19.1/core/payload.ts": "420e17c3c2830b5576ea187cfce77578fe09f1204b25c25ea2f220ca7c86e73b",
|
||||||
"https://deno.land/x/grammy@v1.19.2/filter.ts": "201ddac882ab6cd46cae2d18eb8097460dfe7cedadaab2ba16959c5286d5a5f1",
|
"https://deno.land/x/grammy@v1.19.1/filter.ts": "201ddac882ab6cd46cae2d18eb8097460dfe7cedadaab2ba16959c5286d5a5f1",
|
||||||
"https://deno.land/x/grammy@v1.19.2/mod.ts": "b81cccf69779667b36bef5d0373d1567684917a3b9827873f3de7a7e6af1926f",
|
"https://deno.land/x/grammy@v1.19.1/mod.ts": "b81cccf69779667b36bef5d0373d1567684917a3b9827873f3de7a7e6af1926f",
|
||||||
"https://deno.land/x/grammy@v1.19.2/platform.deno.ts": "84735643c8dde2cf8af5ac2e6b8eb0768452260878da93238d673cb1b4ccea55",
|
"https://deno.land/x/grammy@v1.19.1/platform.deno.ts": "84735643c8dde2cf8af5ac2e6b8eb0768452260878da93238d673cb1b4ccea55",
|
||||||
"https://deno.land/x/grammy@v1.19.2/types.deno.ts": "0f47eacde6d3d65f107f2abf16ecfe726298d30263367cc82e977c801b766229",
|
"https://deno.land/x/grammy@v1.19.1/types.deno.ts": "0f47eacde6d3d65f107f2abf16ecfe726298d30263367cc82e977c801b766229",
|
||||||
"https://deno.land/x/grammy@v1.19.2/types.ts": "729415590dfa188dbe924dea614dff4e976babdbabb28a307b869fc25777cdf0",
|
"https://deno.land/x/grammy@v1.19.1/types.ts": "729415590dfa188dbe924dea614dff4e976babdbabb28a307b869fc25777cdf0",
|
||||||
"https://deno.land/x/grammy_files@v1.0.4/deps.deno.ts": "ec398579e7a7f69788fc3c3ef90202bc9e031e13bfbd90ee37c88a655b6742ef",
|
"https://deno.land/x/grammy_files@v1.0.4/deps.deno.ts": "ec398579e7a7f69788fc3c3ef90202bc9e031e13bfbd90ee37c88a655b6742ef",
|
||||||
"https://deno.land/x/grammy_files@v1.0.4/files.ts": "c69a4cfdcf5b75f32b9de97c36e2b197fe9d60feb360b71e89dff5ae22ba1114",
|
"https://deno.land/x/grammy_files@v1.0.4/files.ts": "c69a4cfdcf5b75f32b9de97c36e2b197fe9d60feb360b71e89dff5ae22ba1114",
|
||||||
"https://deno.land/x/grammy_files@v1.0.4/mod.ts": "379c5f64594cd879653cdce715c318b3eed29ab26e071f356b9b61f9e3943bc3",
|
"https://deno.land/x/grammy_files@v1.0.4/mod.ts": "379c5f64594cd879653cdce715c318b3eed29ab26e071f356b9b61f9e3943bc3",
|
||||||
|
@ -274,10 +273,10 @@
|
||||||
"https://deno.land/x/grammy_runner@v2.0.3/sink.ts": "4c0acb814dee97ffffaea145413b53ccf64c0b00f105b0993383f25111ed8702",
|
"https://deno.land/x/grammy_runner@v2.0.3/sink.ts": "4c0acb814dee97ffffaea145413b53ccf64c0b00f105b0993383f25111ed8702",
|
||||||
"https://deno.land/x/grammy_runner@v2.0.3/source.ts": "68ce5aefa92205db4710c8ac313e0c8e8c2577dd1b88f350b32ee33549188d01",
|
"https://deno.land/x/grammy_runner@v2.0.3/source.ts": "68ce5aefa92205db4710c8ac313e0c8e8c2577dd1b88f350b32ee33549188d01",
|
||||||
"https://deno.land/x/grammy_runner@v2.0.3/worker.ts": "b6f88b9fde15a8dd892c8d4781e8010ef37b80faf04d6ad18db4a43e4faa42ad",
|
"https://deno.land/x/grammy_runner@v2.0.3/worker.ts": "b6f88b9fde15a8dd892c8d4781e8010ef37b80faf04d6ad18db4a43e4faa42ad",
|
||||||
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.5/mod.ts": "2c634ee172fa86da8581ccee02eb43a1af5f377837ee78668dd6dbea446038ab",
|
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.4/mod.ts": "2c634ee172fa86da8581ccee02eb43a1af5f377837ee78668dd6dbea446038ab",
|
||||||
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.5/source/deps.ts": "05a3cae17af42ec058729e14cc544f22eae8bde4792dab652f7a452a00e91301",
|
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.4/source/deps.ts": "4b45ad28d0514ed55d7e2cd54d7d6fefa3cbc7680b38b8f6ce01d25137350deb",
|
||||||
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.5/source/identifier.ts": "162971fdfa5d48484c4db6ce664d60af27affb5e0bfdab41c065200332b30226",
|
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.4/source/identifier.ts": "162971fdfa5d48484c4db6ce664d60af27affb5e0bfdab41c065200332b30226",
|
||||||
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.5/source/index.ts": "b3272436230d7f19f1e9ee515c52e03bbd20ff6963da9b4076d4d3e6935d969d",
|
"https://deno.land/x/grammy_stateless_question_alpha@v3.0.4/source/index.ts": "b3272436230d7f19f1e9ee515c52e03bbd20ff6963da9b4076d4d3e6935d969d",
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/api.ts": "efc90a31eb6f59ae5e7a4cf5838f46529e2fa6fa7e97a51a82dbd28afad21592",
|
"https://deno.land/x/grammy_types@v3.3.0/api.ts": "efc90a31eb6f59ae5e7a4cf5838f46529e2fa6fa7e97a51a82dbd28afad21592",
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/inline.ts": "b5669d79f8c0c6f7d6ca856d548c1ac7d490efd54ee785d18a7c4fc12abfd73b",
|
"https://deno.land/x/grammy_types@v3.3.0/inline.ts": "b5669d79f8c0c6f7d6ca856d548c1ac7d490efd54ee785d18a7c4fc12abfd73b",
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/manage.ts": "e39ec87e74469f70f35aa51dc520b02136ea5e75f9d7a7e0e513846a00b63fd2",
|
"https://deno.land/x/grammy_types@v3.3.0/manage.ts": "e39ec87e74469f70f35aa51dc520b02136ea5e75f9d7a7e0e513846a00b63fd2",
|
||||||
|
@ -289,7 +288,7 @@
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/payment.ts": "d23e9038c5b479b606e620dd84e3e67b6642ada110a962f2d5b5286e99ec7de5",
|
"https://deno.land/x/grammy_types@v3.3.0/payment.ts": "d23e9038c5b479b606e620dd84e3e67b6642ada110a962f2d5b5286e99ec7de5",
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/settings.ts": "5e989f5bd6c587d55673bd8052293869aa2f372e9223dd7f6e28632bfe021b6e",
|
"https://deno.land/x/grammy_types@v3.3.0/settings.ts": "5e989f5bd6c587d55673bd8052293869aa2f372e9223dd7f6e28632bfe021b6e",
|
||||||
"https://deno.land/x/grammy_types@v3.3.0/update.ts": "6d5ec6d1f6d2acf021f807f6bbf7d541487f30672cfab4700e7f935a490c3b78",
|
"https://deno.land/x/grammy_types@v3.3.0/update.ts": "6d5ec6d1f6d2acf021f807f6bbf7d541487f30672cfab4700e7f935a490c3b78",
|
||||||
"https://deno.land/x/indexed_kv@v0.6.1/mod.ts": "6acc28873c392c69eea7be8c70978f1973919dec5ca6c04d91d2221bbaa767f8",
|
"https://deno.land/x/indexed_kv@v0.5.0/mod.ts": "8a0598817202231dbc0be25149d68c21bca82064303270725764814ee43292ea",
|
||||||
"https://deno.land/x/kvfs@v0.1.0/mod.ts": "ceb4c28a6ed850f2fe40bf2349fc8564406811349c7dd6be615db3f098b0790e",
|
"https://deno.land/x/kvfs@v0.1.0/mod.ts": "ceb4c28a6ed850f2fe40bf2349fc8564406811349c7dd6be615db3f098b0790e",
|
||||||
"https://deno.land/x/kvmq@v0.3.0/mod.ts": "ce39abbef6a8f0c25a9141dcac82ef165d4ad74a78289eace7bb168bbc645806",
|
"https://deno.land/x/kvmq@v0.3.0/mod.ts": "ce39abbef6a8f0c25a9141dcac82ef165d4ad74a78289eace7bb168bbc645806",
|
||||||
"https://deno.land/x/lz4@v0.1.2/mod.ts": "4decfc1a3569d03fd1813bd39128b71c8f082850fe98ecfdde20025772916582",
|
"https://deno.land/x/lz4@v0.1.2/mod.ts": "4decfc1a3569d03fd1813bd39128b71c8f082850fe98ecfdde20025772916582",
|
||||||
|
@ -299,91 +298,65 @@
|
||||||
"https://deno.land/x/swc@0.2.1/lib/deno_swc.generated.js": "a112eb5a4871bcbd5b660d3fd9d7afdd5cf2bb9e135eb9b04aceaa24d16481a0",
|
"https://deno.land/x/swc@0.2.1/lib/deno_swc.generated.js": "a112eb5a4871bcbd5b660d3fd9d7afdd5cf2bb9e135eb9b04aceaa24d16481a0",
|
||||||
"https://deno.land/x/swc@0.2.1/mod.ts": "9e03f5daf4c2a25208e34e19ce3e997d2e23a5a11ee8c8ed58023b0e3626073f",
|
"https://deno.land/x/swc@0.2.1/mod.ts": "9e03f5daf4c2a25208e34e19ce3e997d2e23a5a11ee8c8ed58023b0e3626073f",
|
||||||
"https://deno.land/x/ulid@v0.3.0/mod.ts": "f7ff065b66abd485051fc68af23becef6ccc7e81f7774d7fcfd894a4b2da1984",
|
"https://deno.land/x/ulid@v0.3.0/mod.ts": "f7ff065b66abd485051fc68af23becef6ccc7e81f7774d7fcfd894a4b2da1984",
|
||||||
"https://esm.sh/@elysiajs/eden@0.7.4?external=elysia&dev": "28d477942e36cdeb3e57791a3acc1f52dd16378b1e639693897b21e98d7295e3",
|
|
||||||
"https://esm.sh/@elysiajs/swagger@0.7.4?dev": "78520881a756f6c3a69ccc1f306bc32e258c6e0e27f971f68231763fbafd303e",
|
|
||||||
"https://esm.sh/@elysiajs/swagger@0.7.4?external=elysia&dev": "9732dabca93af3dee2ed1781edb740f6e6bdbe88167cc6a03fa42bef3aadd315",
|
|
||||||
"https://esm.sh/@grammyjs/types@2.0.0": "5e5d1ae9f014cb64f0af36fb91f9d35414d670049b77367818a70cd62092b2ac",
|
"https://esm.sh/@grammyjs/types@2.0.0": "5e5d1ae9f014cb64f0af36fb91f9d35414d670049b77367818a70cd62092b2ac",
|
||||||
"https://esm.sh/@grammyjs/types@2.12.1": "eebe448d3bf3d4fdaeacee50e31b9e3947ce965d2591f1036e5c0273cb7aec36",
|
"https://esm.sh/@grammyjs/types@2.12.1": "eebe448d3bf3d4fdaeacee50e31b9e3947ce965d2591f1036e5c0273cb7aec36",
|
||||||
"https://esm.sh/@twind/core@1.1.3": "bf3e64c13de95b061a721831bf2aed322f6e7335848b06d805168c3212686c1d",
|
"https://esm.sh/@twind/core@1.1.3?dev": "e774ab3507f745e78dc57dc719a65c356530e248b3ce848fea6a27f84df9619d",
|
||||||
"https://esm.sh/@twind/preset-tailwind@1.1.4": "29f5e4774c344ff08d2636e3d86382060a8b40d6bce1c158b803df1823c2804c",
|
"https://esm.sh/@twind/preset-tailwind@1.1.4?dev": "3e2b0d9e56cba8a4a5f85273efede31667a820125b67479ca3be354fa335e2ae",
|
||||||
"https://esm.sh/elysia@0.7.21?dev": "6e954350858d5f2fd6c8e61dadf8d9989186f436ccdeb850606359d5e163298c",
|
|
||||||
"https://esm.sh/exifreader@4.14.1": "d0f21973393b0d1a6ed329dac8fcfb2f87ce47fe40b8172e205e7d6d85790bb6",
|
|
||||||
"https://esm.sh/file-type@18.5.0": "f01f6eddb05d365925545e26bd449f934dc042bead882b2795c35c4f48d690a3",
|
"https://esm.sh/file-type@18.5.0": "f01f6eddb05d365925545e26bd449f934dc042bead882b2795c35c4f48d690a3",
|
||||||
"https://esm.sh/openapi-fetch@0.7.6": "43eff6df93e773801cb6ade02b0f47c05d0bfe2fb044adbf2b94fa74ff30d35b",
|
"https://esm.sh/openapi-fetch@0.7.6": "43eff6df93e773801cb6ade02b0f47c05d0bfe2fb044adbf2b94fa74ff30d35b",
|
||||||
"https://esm.sh/react-dom@18.2.0/client?external=react&dev": "2eb39c339720d727591fd55fb44ffcb6f14b06812af0a71e7a2268185b5b6e73",
|
"https://esm.sh/png-chunk-text@1.0.0": "08beb86f31b5ff70240650fe095b6c4e4037e5c1d5917f9338e7633cae680356",
|
||||||
"https://esm.sh/react-flip-move@3.0.5?external=react&dev": "4390c0777a0bec583d3e6cb5e4b33831ac937d670d894a20e4f192ce8cd21bae",
|
"https://esm.sh/png-chunks-extract@1.0.0": "da06bbd3c08199d72ab16354abe5ffd2361cb891ccbb44d757a0a0a4fbfa12b5",
|
||||||
"https://esm.sh/react-intl@6.4.7?external=react&dev": "60e68890e2c5ef3c02d37a89c53e056b1bbd1c8467a7aae62f0b634abc7a8a5f",
|
"https://esm.sh/react-dom@18.2.0/client?dev": "db94655481962a38b32ffbbb0ad50df0a5582ee3e139acb375f989f74b630d82",
|
||||||
"https://esm.sh/react-router-dom@6.16.0?external=react&dev": "16046eba15c1ae1ce912e5ab6fdd1f6ce24ea8ac3970d5bdcdb1ebb0e40458c2",
|
"https://esm.sh/react-flip-move@3.0.5?dev": "f92489b69efcaba985c9f45b4f4888a24aee7076cf5c03dad378e103d0c1e492",
|
||||||
|
"https://esm.sh/react-intl@6.4.7?external=react&alias=@types/react:react&dev": "7b36a3ea6a607b5d20409129fb43541e665fbcd3607fb8d54541b66a6199b1a2",
|
||||||
|
"https://esm.sh/react-router-dom@6.16.0?dev": "da2bc1a0702667487b3724590e225fd4ea62e8ef8645b06eefbe29b64e6ad161",
|
||||||
"https://esm.sh/react@18.2.0?dev": "1fe185734f3d77826efb78a22779899d143bb1160baaf5bbf01edee9f4eaa9d5",
|
"https://esm.sh/react@18.2.0?dev": "1fe185734f3d77826efb78a22779899d143bb1160baaf5bbf01edee9f4eaa9d5",
|
||||||
"https://esm.sh/stable/react@18.2.0/denonext/react.development.mjs": "88729b33eccbea2c09894f07459f32911777721a3b50fd32151182e82c4351e2",
|
"https://esm.sh/stable/react@18.2.0/denonext/react.development.mjs": "88729b33eccbea2c09894f07459f32911777721a3b50fd32151182e82c4351e2",
|
||||||
"https://esm.sh/swr@2.2.4?external=react&dev": "e96c7c09e01c12fca1d871935973c7264353d8a1a1630fdf38ca1b364936ca66",
|
"https://esm.sh/swr@2.2.4?dev": "51d948a1c62a540e833c2ee9c5be77474781a7fcb491b3e2fc6987a64280ffb7",
|
||||||
"https://esm.sh/telegram-format@2.1.0": "f7302f17a2fbd4ef793b7c065116a2994861904520d54126926bb910836a44b9",
|
"https://esm.sh/ty-rest@0.4.0/client?dev": "536ada606fc8d34ae6edf7799755ff73a1c7573f945aa0ebece57d032d776ba6",
|
||||||
"https://esm.sh/ty-rest@0.4.0/client": "66481393ac52b435a8b6e28d75ac2aaf95460d98a54308b718fe35c8d23e5e84",
|
"https://esm.sh/ty-rest@0.4.0/server?dev": "c0db37ac0313d1ceed63791b165dd234335c16e718b23026171db9b5595664ee",
|
||||||
"https://esm.sh/ty-rest@0.4.0/server": "14dc9ef120306504751b0faa1e436cd29fb231dae0c2518e1cf564326982c48c",
|
"https://esm.sh/use-local-storage@3.0.0?dev": "ddecd8b50fdb196ee3520f2d4f656069bd550833c701af8333ee348923b4ef18",
|
||||||
"https://esm.sh/ty-rest@0.4.1/client": "bb9bf7c41c824920272aeefb9fa62c4a180ef2d56217c02eb16000f928d85013",
|
"https://esm.sh/v133/@formatjs/ecma402-abstract@1.17.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/ecma402-abstract.development.mjs": "6bc8e991e96ec1a0c91598525756a528a7b4927d570eef2b19caaaccc1fcd8bf",
|
||||||
"https://esm.sh/ty-rest@0.4.1/server": "9335b9acf42dd3d3e3ffacc6f420eda4805c606d409e6e0a2b724fcddeea8b72",
|
"https://esm.sh/v133/@formatjs/fast-memoize@2.2.0/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/fast-memoize.development.mjs": "4c027b3308490b65dc899f683b1ff9be8da4b7a2e1e32433ef03bcb8f0fdf821",
|
||||||
"https://esm.sh/use-local-storage@3.0.0?external=react&dev": "4cf7fce754a9488940daa76051389421c6341420cae5b8c7d2401158ffe79ec0",
|
"https://esm.sh/v133/@formatjs/icu-messageformat-parser@2.6.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/icu-messageformat-parser.development.mjs": "30783c1741a478baa30ebf9a71ea0e2416a706265e14a229b0992058dfd6687e",
|
||||||
"https://esm.sh/v132/@twind/core@1.1.3/denonext/core.mjs": "c2618087a5d5cc406c7dc1079015f4d7cc874bee167f74e9945694896d907b6d",
|
"https://esm.sh/v133/@formatjs/icu-skeleton-parser@1.6.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/icu-skeleton-parser.development.mjs": "85a61304c66fe8cc15da3899b79df44864c0f1a9dea73d6b23bbf944c973ff64",
|
||||||
"https://esm.sh/v132/@twind/preset-tailwind@1.1.4/denonext/preset-tailwind.mjs": "3cb9f5cde89e11cd2adad54ff264f62f5000ccb1694cd88874b1856eb2d8d7f7",
|
"https://esm.sh/v133/@formatjs/intl-localematcher@0.4.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/intl-localematcher.development.mjs": "26325e3fc4b1728583a863c46332b6878c377bbb918a39896eb0d9f39eb41357",
|
||||||
"https://esm.sh/v133/@elysiajs/swagger@0.7.4/denonext/swagger.development.mjs": "63cc67eee29123918283027744da23009fc3767088c513f8bfbfef945b7170d1",
|
"https://esm.sh/v133/@formatjs/intl@2.9.3/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/intl.development.mjs": "b01ab881505e8d29bf9b337f9b48841893203e063d31775eed6ba292acfac298",
|
||||||
"https://esm.sh/v133/@formatjs/ecma402-abstract@1.17.2/X-ZS9yZWFjdA/denonext/ecma402-abstract.development.mjs": "178a303e29d73369b4c7c1da5a18393c2baa8742a0e8a45be85f506c17f763d9",
|
|
||||||
"https://esm.sh/v133/@formatjs/fast-memoize@2.2.0/X-ZS9yZWFjdA/denonext/fast-memoize.development.mjs": "4c027b3308490b65dc899f683b1ff9be8da4b7a2e1e32433ef03bcb8f0fdf821",
|
|
||||||
"https://esm.sh/v133/@formatjs/icu-messageformat-parser@2.6.2/X-ZS9yZWFjdA/denonext/icu-messageformat-parser.development.mjs": "68c7a9be44aaa3e35bfe18668e17a2d4a465e11f59f3a1e025ee83fe1bd0b971",
|
|
||||||
"https://esm.sh/v133/@formatjs/icu-skeleton-parser@1.6.2/X-ZS9yZWFjdA/denonext/icu-skeleton-parser.development.mjs": "278cf26f11f5c4027a0bbd503832b162be979d0f4732e3b716760fc5543ecfac",
|
|
||||||
"https://esm.sh/v133/@formatjs/intl-localematcher@0.4.2/X-ZS9yZWFjdA/denonext/intl-localematcher.development.mjs": "26325e3fc4b1728583a863c46332b6878c377bbb918a39896eb0d9f39eb41357",
|
|
||||||
"https://esm.sh/v133/@formatjs/intl@2.9.3/X-ZS9yZWFjdA/denonext/intl.development.mjs": "42e7cf0cc4f4c3bca21956caefbd17b64fcb01ac07cb4a36d8a301188dcf1fce",
|
|
||||||
"https://esm.sh/v133/@grammyjs/types@2.0.0/denonext/types.mjs": "7ee61bd0c55a152ea1ffaf3fbe63fce6c103ae836265b23290284d6ba0e3bc5b",
|
"https://esm.sh/v133/@grammyjs/types@2.0.0/denonext/types.mjs": "7ee61bd0c55a152ea1ffaf3fbe63fce6c103ae836265b23290284d6ba0e3bc5b",
|
||||||
"https://esm.sh/v133/@grammyjs/types@2.12.1/denonext/types.mjs": "3636f7a1ca7fef89fa735d832b72193a834bc7f5250b6bf182544be53a6ab218",
|
"https://esm.sh/v133/@grammyjs/types@2.12.1/denonext/types.mjs": "3636f7a1ca7fef89fa735d832b72193a834bc7f5250b6bf182544be53a6ab218",
|
||||||
"https://esm.sh/v133/@remix-run/router@1.9.0/X-ZS9yZWFjdA/denonext/router.development.mjs": "97f96f031a7298b0afbbadb23177559ee9b915314d7adf0b9c6a8d7f452c70e7",
|
"https://esm.sh/v133/@remix-run/router@1.9.0/denonext/router.development.mjs": "97f96f031a7298b0afbbadb23177559ee9b915314d7adf0b9c6a8d7f452c70e7",
|
||||||
"https://esm.sh/v133/@sinclair/typebox@0.31.22/denonext/compiler.development.js": "94129b8b2f26f79bd1ec51f479776e7cdebe521f60bd2eae695634765959f7b9",
|
"https://esm.sh/v133/@twind/core@1.1.3/denonext/core.development.mjs": "1dd38a506c4b728456796e87caa55a31c3855edc6a052a391264c96ee6b89f67",
|
||||||
"https://esm.sh/v133/@sinclair/typebox@0.31.22/denonext/system.development.js": "e2c96ea22f15b9e79e8021b65ae62bfd1b4fd94176dd9c8fc1042fa014c64e9e",
|
"https://esm.sh/v133/@twind/preset-tailwind@1.1.4/denonext/preset-tailwind.development.mjs": "fc8acfed444337d323d8d6ab5d6198cbc4573a26816387c66ebf6524d20bd772",
|
||||||
"https://esm.sh/v133/@sinclair/typebox@0.31.22/denonext/typebox.development.mjs": "d5776065180eea03471f3f2c992a0cf8289219881c40b68270f543536f5ee029",
|
"https://esm.sh/v133/client-only@0.0.1/denonext/client-only.development.mjs": "b7efaef2653f7f628d084e24a4d5a857c9cd1a5e5060d1d9957e185ee98c8a28",
|
||||||
"https://esm.sh/v133/@sinclair/typebox@0.31.22/denonext/value.development.js": "41db30d8ba66c836ad5faf931f7ce38d15f943339847b8a7491014f8fe2be451",
|
|
||||||
"https://esm.sh/v133/client-only@0.0.1/X-ZS9yZWFjdA/denonext/client-only.development.mjs": "b7efaef2653f7f628d084e24a4d5a857c9cd1a5e5060d1d9957e185ee98c8a28",
|
|
||||||
"https://esm.sh/v133/cookie@0.5.0/denonext/cookie.development.mjs": "04344caac1f8341ced6ec11d15b95a36dbda70a8ebb6a809b20b5912a5de2b8b",
|
|
||||||
"https://esm.sh/v133/crc-32@0.3.0/denonext/crc-32.mjs": "92bbd96cd5a92e45267cf4b3d3928b9355d16963da1ba740637fb10f1daca590",
|
"https://esm.sh/v133/crc-32@0.3.0/denonext/crc-32.mjs": "92bbd96cd5a92e45267cf4b3d3928b9355d16963da1ba740637fb10f1daca590",
|
||||||
"https://esm.sh/v133/elysia@0.7.21/denonext/elysia.development.mjs": "4bf283f62b935737d33ab4e3e59195ba9cb0ea2fc2bac90c8e9fa00a44b5e575",
|
|
||||||
"https://esm.sh/v133/eventemitter3@5.0.1/denonext/eventemitter3.development.mjs": "e19a3e8d30d7564ce7b394636fc66ef273c6394bb6c5820b9a9bfba7ec4ac2dd",
|
|
||||||
"https://esm.sh/v133/exifreader@4.14.1/denonext/exifreader.mjs": "691e1c1d1337ccaf092bf39115fdac56bf69e93259d04bb03985e918202317ab",
|
|
||||||
"https://esm.sh/v133/fast-decode-uri-component@1.0.1/denonext/fast-decode-uri-component.development.mjs": "004912e1f391fccf376cf58ff1ab06d6d4ed241cab9c7bed23756091bedbdc36",
|
|
||||||
"https://esm.sh/v133/fast-querystring@1.1.2/denonext/fast-querystring.development.mjs": "da06ef49d7e834dbac2b3b189de02c80e71cd5942b339de0011ab84aae0de0ff",
|
|
||||||
"https://esm.sh/v133/file-type@18.5.0/denonext/file-type.mjs": "785cac1bc363448647871e1b310422e01c9cfb7b817a68690710b786d3598311",
|
"https://esm.sh/v133/file-type@18.5.0/denonext/file-type.mjs": "785cac1bc363448647871e1b310422e01c9cfb7b817a68690710b786d3598311",
|
||||||
"https://esm.sh/v133/hoist-non-react-statics@3.3.2/X-ZS9yZWFjdA/denonext/hoist-non-react-statics.development.mjs": "fcafac9e3c33810f18ecb43dfc32ce80efc88adc63d3f49fd7ada0665d2146c6",
|
"https://esm.sh/v133/hoist-non-react-statics@3.3.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/hoist-non-react-statics.development.mjs": "6e66f502f9c4bfeadf9edaa8613965eab0dadab8c22cc83afa4f9d238cdb5153",
|
||||||
"https://esm.sh/v133/ieee754@1.2.1/denonext/ieee754.mjs": "9ec2806065f50afcd4cf3f3f2f38d93e777a92a5954dda00d219f57d4b24832f",
|
"https://esm.sh/v133/ieee754@1.2.1/denonext/ieee754.mjs": "9ec2806065f50afcd4cf3f3f2f38d93e777a92a5954dda00d219f57d4b24832f",
|
||||||
"https://esm.sh/v133/inherits@2.0.4/denonext/inherits.mjs": "8095f3d6aea060c904fb24ae50f2882779c0acbe5d56814514c8b5153f3b4b3b",
|
"https://esm.sh/v133/inherits@2.0.4/denonext/inherits.mjs": "8095f3d6aea060c904fb24ae50f2882779c0acbe5d56814514c8b5153f3b4b3b",
|
||||||
"https://esm.sh/v133/intl-messageformat@10.5.3/X-ZS9yZWFjdA/denonext/intl-messageformat.development.mjs": "d6b701c562c51ec4b4e1deb641c6623986abe304290bbb291e6404cb6a31dc41",
|
"https://esm.sh/v133/intl-messageformat@10.5.3/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/intl-messageformat.development.mjs": "8c995acc5e8423a90576dfce8c00b181243987f1ff9922cf6162da4c228e6392",
|
||||||
"https://esm.sh/v133/lodash.clonedeep@4.5.0/denonext/lodash.clonedeep.development.mjs": "f44c863cb41bcd2714103a36d97e8eb8268f56070b5cd56dcfae26c556e6622e",
|
|
||||||
"https://esm.sh/v133/memoirist@0.1.4/denonext/memoirist.development.mjs": "e19f8379a684345ebcab9a688b0b188f29120f4bf1122588b779113cd1ec0e4e",
|
|
||||||
"https://esm.sh/v133/openapi-fetch@0.7.6/denonext/openapi-fetch.mjs": "1ec8ed23c9141c7f4e58de06f84525e310fe7dda1aeaf675c8edafb3d8292cfc",
|
"https://esm.sh/v133/openapi-fetch@0.7.6/denonext/openapi-fetch.mjs": "1ec8ed23c9141c7f4e58de06f84525e310fe7dda1aeaf675c8edafb3d8292cfc",
|
||||||
"https://esm.sh/v133/peek-readable@5.0.0/denonext/peek-readable.mjs": "5e799ea86e9c501873f687eda9c891a75ed55ba666b5dd822eaa3d28a8a5f2b1",
|
"https://esm.sh/v133/peek-readable@5.0.0/denonext/peek-readable.mjs": "5e799ea86e9c501873f687eda9c891a75ed55ba666b5dd822eaa3d28a8a5f2b1",
|
||||||
"https://esm.sh/v133/png-chunk-text@1.0.0/denonext/png-chunk-text.mjs": "e8bb89595ceab2603531693319da08a9dd90d51169437de47a73cf8bac7baa11",
|
"https://esm.sh/v133/png-chunk-text@1.0.0/denonext/png-chunk-text.mjs": "e8bb89595ceab2603531693319da08a9dd90d51169437de47a73cf8bac7baa11",
|
||||||
"https://esm.sh/v133/png-chunks-extract@1.0.0/denonext/png-chunks-extract.mjs": "2a9b62c9478e2bb79f7ccc9725be3b79afa473089ac978d8fd14a8b0cba7c040",
|
"https://esm.sh/v133/png-chunks-extract@1.0.0/denonext/png-chunks-extract.mjs": "2a9b62c9478e2bb79f7ccc9725be3b79afa473089ac978d8fd14a8b0cba7c040",
|
||||||
"https://esm.sh/v133/react-dom@18.2.0/X-ZS9yZWFjdA/denonext/client.development.js": "ebaa7a1fce9f40b8abdae0daab06348b7bbdf5a90e1e60d2116b45d3577f8fa4",
|
"https://esm.sh/v133/react-dom@18.2.0/denonext/client.development.js": "372d345cb97c5ec7b9cd9042d49bdea157c2a231db8078c4857ca9354e1a8da0",
|
||||||
"https://esm.sh/v133/react-dom@18.2.0/X-ZS9yZWFjdA/denonext/react-dom.development.mjs": "b7e7937e4f3446bb2b6db5f73ae20d2b18a4116330486680c6c6f610f7d85f27",
|
"https://esm.sh/v133/react-dom@18.2.0/denonext/react-dom.development.mjs": "a16c087a0cdb5b98c3eaf44c5d24a680e21fc69a8b144859e14ebdd1849755d7",
|
||||||
"https://esm.sh/v133/react-flip-move@3.0.5/X-ZS9yZWFjdA/denonext/react-flip-move.development.mjs": "4644ea8644c7d6ccb3563fec092d05b82c647da4981e878b64fd06ab58d6cfc5",
|
"https://esm.sh/v133/react-flip-move@3.0.5/denonext/react-flip-move.development.mjs": "1a139b6c46af5e4e5302b1c748e80bfd03144989e8c81dea4fbd83dfb6cf4b57",
|
||||||
"https://esm.sh/v133/react-intl@6.4.7/X-ZS9yZWFjdA/denonext/react-intl.development.mjs": "866e2105594ecf772144262d215ffd3245f09e8f1b14e97b5f13b4df8d52c419",
|
"https://esm.sh/v133/react-intl@6.4.7/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/react-intl.development.mjs": "9017a5dc9316c3f068eb81794f69ac5a6944e649dcec4a45bad2826c0352fd69",
|
||||||
"https://esm.sh/v133/react-is@16.13.1/X-ZS9yZWFjdA/denonext/react-is.development.mjs": "9e2c3272e256b176f71660a52bf0f8a079babbd01e680de070d47ed51a9319bd",
|
"https://esm.sh/v133/react-is@16.13.1/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/react-is.development.mjs": "9e2c3272e256b176f71660a52bf0f8a079babbd01e680de070d47ed51a9319bd",
|
||||||
"https://esm.sh/v133/react-router-dom@6.16.0/X-ZS9yZWFjdA/denonext/react-router-dom.development.mjs": "0b2d33abc84446dcf3430a42a1a7935b9a49f81192cba22b1263a884c6edf22d",
|
"https://esm.sh/v133/react-router-dom@6.16.0/denonext/react-router-dom.development.mjs": "4c4022a51af6e07f477e454d0bf3a4a085a41ccf21d76b59663533d717d86f66",
|
||||||
"https://esm.sh/v133/react-router@6.16.0/X-ZS9yZWFjdA/denonext/react-router.development.mjs": "e539a1882ef472de5bd8e4325a54e0f0d36a45a0bf53d4ffed09353225a2cf95",
|
"https://esm.sh/v133/react-router@6.16.0/denonext/react-router.development.mjs": "c7f1443519d01e5791bc35f64cb561be72e3473060a1e14844b47c50d5e02223",
|
||||||
"https://esm.sh/v133/readable-stream@3.6.2/denonext/readable-stream.mjs": "4d368fe1058f90ecfb37581103cae1646123180b8fc4aa77157fd733aed24e4a",
|
"https://esm.sh/v133/readable-stream@3.6.2/denonext/readable-stream.mjs": "4d368fe1058f90ecfb37581103cae1646123180b8fc4aa77157fd733aed24e4a",
|
||||||
"https://esm.sh/v133/readable-web-to-node-stream@3.0.2/denonext/readable-web-to-node-stream.mjs": "8e7f8b7139f71b1cf94cb4d4772239755e996d88bcefb462f03fd6a0b8b4cd83",
|
"https://esm.sh/v133/readable-web-to-node-stream@3.0.2/denonext/readable-web-to-node-stream.mjs": "8e7f8b7139f71b1cf94cb4d4772239755e996d88bcefb462f03fd6a0b8b4cd83",
|
||||||
"https://esm.sh/v133/scheduler@0.23.0/X-ZS9yZWFjdA/denonext/scheduler.development.mjs": "b6c8f513ecc4afb69b0c5c78a7674bb1c78f7e003946c6c8c655778d81d775db",
|
"https://esm.sh/v133/scheduler@0.23.0/denonext/scheduler.development.mjs": "b6c8f513ecc4afb69b0c5c78a7674bb1c78f7e003946c6c8c655778d81d775db",
|
||||||
"https://esm.sh/v133/strtok3@7.0.0/denonext/core.js": "4934052fd9086facbb9436e905dfdee19ee27a43d0b0a8fca39648e833324577",
|
"https://esm.sh/v133/strtok3@7.0.0/denonext/core.js": "4934052fd9086facbb9436e905dfdee19ee27a43d0b0a8fca39648e833324577",
|
||||||
"https://esm.sh/v133/swr@2.2.4/X-ZS9yZWFjdA/denonext/_internal.development.js": "e64b6b30de0ddca08078301c3129c608ab8f2cc6279c704c462ebe3022e81275",
|
"https://esm.sh/v133/swr@2.2.4/denonext/_internal.development.js": "a9cec421ab0aaa32d9c7e5b55fd73b6d34b4331c71a58e0deab37437c7f263d8",
|
||||||
"https://esm.sh/v133/swr@2.2.4/X-ZS9yZWFjdA/denonext/swr.development.mjs": "d6be45253b10e1f6c2bd35f035530d5ac0e7c34c716508db131a143fb9818a82",
|
"https://esm.sh/v133/swr@2.2.4/denonext/swr.development.mjs": "edbaabd9f0a72387a39fb09bcac308adde07f8b7a756d4b7c5cdc6aa79ec9a4d",
|
||||||
"https://esm.sh/v133/telegram-format@2.1.0/denonext/telegram-format.mjs": "f5b6a6df788e9b462ce3e0d3db8fb3446bfa5252440b9878a91496ca6fb2d693",
|
|
||||||
"https://esm.sh/v133/token-types@5.0.1/denonext/token-types.mjs": "eb8ef626bdfc077ae8f9b6c0842e3bdae25b4867dd9d38b7b1b5f003819a06d3",
|
"https://esm.sh/v133/token-types@5.0.1/denonext/token-types.mjs": "eb8ef626bdfc077ae8f9b6c0842e3bdae25b4867dd9d38b7b1b5f003819a06d3",
|
||||||
"https://esm.sh/v133/tslib@2.6.2/X-ZS9yZWFjdA/denonext/tslib.development.mjs": "40dea6e88a1261c5c96a641d73276ffecae3f2d23b9a93d1c5294125f4474cdb",
|
"https://esm.sh/v133/tslib@2.6.2/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/tslib.development.mjs": "40dea6e88a1261c5c96a641d73276ffecae3f2d23b9a93d1c5294125f4474cdb",
|
||||||
"https://esm.sh/v133/ty-rest@0.4.0/denonext/client.js": "dd2a1f04d17c87b395d7a39789e41477e854afcfb66559be08348ad76625615b",
|
"https://esm.sh/v133/ty-rest@0.4.0/denonext/client.development.js": "00f1de0ffd56afd5da29d786c73c35820f1493c7a81d2c12c006449841228c9b",
|
||||||
"https://esm.sh/v133/ty-rest@0.4.0/denonext/server.js": "98f33c86c304659703f3a1294ae93e78920d8de303dc0f4e360edd717a7bc667",
|
"https://esm.sh/v133/ty-rest@0.4.0/denonext/server.development.js": "e5cee0108b4abbe0b1a7d336c15aaa5955c313503088ebbbc393a05a031265bf",
|
||||||
"https://esm.sh/v133/ty-rest@0.4.1/denonext/client.js": "466aeedd7bb139c85fb3051f0b7cecaf3e3afac98b4825b98a480f8a127692ed",
|
"https://esm.sh/v133/use-local-storage@3.0.0/denonext/use-local-storage.development.mjs": "72e24a28434c3de0024ba46b62517a74611c6ce0872468f6890b3cbfbfa21a07",
|
||||||
"https://esm.sh/v133/ty-rest@0.4.1/denonext/server.js": "00be1165ac96313b077629556a1587d4662f4e23bb0e815b945cc95cd3582370",
|
"https://esm.sh/v133/use-sync-external-store@1.2.0/denonext/shim.development.js": "6d809e52639ca720eee46a6c63ba55c597eb72d7eae1edd95cf3643538e46158",
|
||||||
"https://esm.sh/v133/use-local-storage@3.0.0/X-ZS9yZWFjdA/denonext/use-local-storage.development.mjs": "1fdc00893fe7dac56e95e2817e05d413b674f5cb5a1c6afd8994e25c9e2a56c8",
|
"https://esm.sh/v133/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "f69f67cf655c38428b0934e0f7c865c055834a87cc3866b629d6b2beb21005e9"
|
||||||
"https://esm.sh/v133/use-sync-external-store@1.2.0/X-ZS9yZWFjdA/denonext/shim.development.js": "5388baf48494f5abe76f8a4a30810c48e828b52f1298826aa9a3f3378e2b533f",
|
|
||||||
"https://esm.sh/v133/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "f69f67cf655c38428b0934e0f7c865c055834a87cc3866b629d6b2beb21005e9",
|
|
||||||
"https://esm.sh/v134/@elysiajs/eden@0.7.4/X-ZS9lbHlzaWE/denonext/eden.development.mjs": "c79c9f105d2b2062b882240272faa81cab1cbdde1113290d2d56c15ed9479986",
|
|
||||||
"https://esm.sh/v134/@elysiajs/swagger@0.7.4/X-ZS9lbHlzaWE/denonext/swagger.development.mjs": "4aa74e7b8108e9bacd7830c374ef24ee1b4100d65eef045bf6802ceaa3635554",
|
|
||||||
"https://esm.sh/v134/@sinclair/typebox@0.31.26/X-ZS9lbHlzaWE/denonext/typebox.development.mjs": "5f24db9ca594ccb61f2ae58df80a1f7690d21e117772e7b00babdb290c0747df",
|
|
||||||
"https://esm.sh/v134/lodash.clonedeep@4.5.0/X-ZS9lbHlzaWE/denonext/lodash.clonedeep.development.mjs": "f44c863cb41bcd2714103a36d97e8eb8268f56070b5cd56dcfae26c556e6622e"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
main.ts
7
main.ts
|
@ -4,9 +4,7 @@ import { ConsoleHandler } from "std/log/handlers.ts";
|
||||||
import { LevelName, setup } from "std/log/mod.ts";
|
import { LevelName, setup } from "std/log/mod.ts";
|
||||||
import { serveUi } from "./api/mod.ts";
|
import { serveUi } from "./api/mod.ts";
|
||||||
import { runAllTasks } from "./app/mod.ts";
|
import { runAllTasks } from "./app/mod.ts";
|
||||||
// runbot shouldn't be needed for webhooks?
|
import { runBot } from "./bot/mod.ts";
|
||||||
// import { runBot } from "./bot/mod.ts";
|
|
||||||
import "./bot/mod.ts";
|
|
||||||
|
|
||||||
const logLevel = Deno.env.get("LOG_LEVEL")?.toUpperCase() as LevelName ?? "INFO";
|
const logLevel = Deno.env.get("LOG_LEVEL")?.toUpperCase() as LevelName ?? "INFO";
|
||||||
|
|
||||||
|
@ -22,8 +20,7 @@ setup({
|
||||||
|
|
||||||
// run parts of the app
|
// run parts of the app
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// runbot shouldn't be needed for webhooks?
|
runBot(),
|
||||||
// runBot(),
|
|
||||||
runAllTasks(),
|
runAllTasks(),
|
||||||
serveUi(),
|
serveUi(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
These scripts exist to automatically restart the bot in case of errors and connectivity issues. They should either be run as systemd services or cronjobs.
|
|
|
@ -1,29 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This script has to be run as systemd service. It checks journalctl output for errors and restarts the bot automatically when they occur too often.
|
|
||||||
|
|
||||||
# Define the regex pattern to search for occurrences of "GrammyError" and "sendMediaGroup"
|
|
||||||
ERROR_PATTERN="GrammyError.*sendMediaGroup"
|
|
||||||
|
|
||||||
# Initialize the counter for consecutive occurrences
|
|
||||||
error_count=0
|
|
||||||
|
|
||||||
# Monitor the journalctl output
|
|
||||||
journalctl -xe -f | while read line; do
|
|
||||||
# Check if the line contains the pattern
|
|
||||||
if echo "$line" | grep -qE "$ERROR_PATTERN"; then
|
|
||||||
# Increment the error counter
|
|
||||||
((error_count++))
|
|
||||||
|
|
||||||
# Check if the error has occurred 4 times in a row
|
|
||||||
if [ $error_count -eq 4 ]; then
|
|
||||||
# Restart the bot service
|
|
||||||
systemctl restart nyxthebot
|
|
||||||
# Reset the error counter
|
|
||||||
error_count=0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Reset the error counter if the line does not contain the error
|
|
||||||
error_count=0
|
|
||||||
fi
|
|
||||||
done
|
|
|
@ -1,30 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# When the bot gets eepy sleepy, this script will restart it. It happens, when no jobs come in for multiple minutes.
|
|
||||||
# The script interfaces with the REST API of the bot and checks if the image count has changed.
|
|
||||||
# Run this script as cronjob all 4-5 minutes.
|
|
||||||
|
|
||||||
# Define the URL and the stats file path
|
|
||||||
URL="https://YOUR_URL/api/stats"
|
|
||||||
STATS_FILE="YOUR_FILE_PATH/stats.txt"
|
|
||||||
|
|
||||||
# Fetch the data and extract imageCount
|
|
||||||
imageCount=$(curl -s "$URL" | jq '.imageCount')
|
|
||||||
|
|
||||||
# Check if stats file exists and read the last value
|
|
||||||
if [ -f "$STATS_FILE" ]; then
|
|
||||||
lastValue=$(tail -n 1 "$STATS_FILE")
|
|
||||||
else
|
|
||||||
lastValue=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Save the new value to the file
|
|
||||||
echo "$imageCount" >> "$STATS_FILE"
|
|
||||||
|
|
||||||
# Keep only the last 2 values in the file
|
|
||||||
tail -n 2 "$STATS_FILE" > "$STATS_FILE.tmp" && mv "$STATS_FILE.tmp" "$STATS_FILE"
|
|
||||||
|
|
||||||
# Compare the two values and restart the service if they are the same
|
|
||||||
if [ "$lastValue" == "$imageCount" ]; then
|
|
||||||
systemctl restart nyxthebot
|
|
||||||
fi
|
|
|
@ -1,242 +0,0 @@
|
||||||
import React, { useRef } from "react";
|
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
|
||||||
import { AdminData } from "../api/adminsRoute.ts";
|
|
||||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
|
||||||
|
|
||||||
export function AdminsPage(props: { sessionId: string | null }) {
|
|
||||||
const { sessionId } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
const addDialogRef = useRef<HTMLDialogElement>(null);
|
|
||||||
|
|
||||||
const getSession = useSWR(
|
|
||||||
sessionId ? ["/sessions/:sessionId", { params: { sessionId } }] as const : null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
const getUser = useSWR(
|
|
||||||
getSession.data?.userId
|
|
||||||
? ["/users/:userId", { params: { userId: String(getSession.data.userId) } }] as const
|
|
||||||
: null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAdmins = useSWR(
|
|
||||||
["/admins", { method: "GET" }] as const,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{getUser.data && getAdmins.data && getAdmins.data.length === 0 && (
|
|
||||||
<button
|
|
||||||
className="button-filled"
|
|
||||||
onClick={() => {
|
|
||||||
fetchApi("/admins/promote_self", {
|
|
||||||
method: "POST",
|
|
||||||
query: { sessionId: sessionId ?? "" },
|
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Promote me to admin
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{getAdmins.data?.length
|
|
||||||
? (
|
|
||||||
<ul className="flex flex-col gap-2">
|
|
||||||
{getAdmins.data.map((admin) => (
|
|
||||||
<AdminListItem key={admin.tgUserId} admin={admin} sessionId={sessionId} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
: getAdmins.data?.length === 0
|
|
||||||
? (
|
|
||||||
<ul className="flex flex-col gap-2">
|
|
||||||
<li className="flex flex-col gap-2 rounded-md bg-zinc-100 dark:bg-zinc-800 p-2">
|
|
||||||
<p key="no-admins" className="text-center text-gray-500">No admins.</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
: getAdmins.error
|
|
||||||
? <p className="alert">Loading admins failed</p>
|
|
||||||
: <div className="spinner self-center" />}
|
|
||||||
|
|
||||||
{getUser.data?.admin && (
|
|
||||||
<button
|
|
||||||
className="button-filled"
|
|
||||||
onClick={() => addDialogRef.current?.showModal()}
|
|
||||||
>
|
|
||||||
Add admin
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AddAdminDialog
|
|
||||||
dialogRef={addDialogRef}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddAdminDialog(props: {
|
|
||||||
dialogRef: React.RefObject<HTMLDialogElement>;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, sessionId } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog ref={dialogRef} className="dialog animate-pop-in backdrop-animate-fade-in">
|
|
||||||
<form
|
|
||||||
method="dialog"
|
|
||||||
className="flex flex-col gap-4 p-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target as HTMLFormElement);
|
|
||||||
fetchApi("/admins", {
|
|
||||||
method: "POST",
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
body: {
|
|
||||||
tgUserId: Number(data.get("tgUserId") as string),
|
|
||||||
},
|
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
|
||||||
dialogRef.current?.close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
Telegram user ID
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="text"
|
|
||||||
name="tgUserId"
|
|
||||||
required
|
|
||||||
pattern="-?\d+"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-outlined"
|
|
||||||
onClick={() => dialogRef.current?.close()}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="button-filled">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AdminListItem(props: { admin: AdminData; sessionId: string | null }) {
|
|
||||||
const { admin, sessionId } = props;
|
|
||||||
|
|
||||||
const deleteDialogRef = useRef<HTMLDialogElement>(null);
|
|
||||||
|
|
||||||
const getAdminUser = useSWR(
|
|
||||||
["/users/:userId", { params: { userId: String(admin.tgUserId) } }] as const,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSession = useSWR(
|
|
||||||
sessionId ? ["/sessions/:sessionId", { params: { sessionId } }] as const : null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
const getUser = useSWR(
|
|
||||||
getSession.data?.userId
|
|
||||||
? ["/users/:userId", { params: { userId: String(getSession.data.userId) } }] as const
|
|
||||||
: null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="flex flex-col gap-2 rounded-md bg-zinc-100 dark:bg-zinc-800 p-2">
|
|
||||||
<p className="font-bold">
|
|
||||||
{getAdminUser.data?.first_name ?? admin.tgUserId} {getAdminUser.data?.last_name}{" "}
|
|
||||||
{getAdminUser.data?.username
|
|
||||||
? (
|
|
||||||
<a href={`https://t.me/${getAdminUser.data.username}`} className="link">
|
|
||||||
@{getAdminUser.data.username}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</p>
|
|
||||||
{getAdminUser.data?.bio
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
{getAdminUser.data?.bio}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{getUser.data?.admin && (
|
|
||||||
<p className="flex gap-2">
|
|
||||||
<button
|
|
||||||
className="button-outlined"
|
|
||||||
onClick={() => deleteDialogRef.current?.showModal()}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<DeleteAdminDialog
|
|
||||||
dialogRef={deleteDialogRef}
|
|
||||||
adminId={admin.tgUserId}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteAdminDialog(props: {
|
|
||||||
dialogRef: React.RefObject<HTMLDialogElement>;
|
|
||||||
adminId: number;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, adminId, sessionId } = props;
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog
|
|
||||||
ref={dialogRef}
|
|
||||||
className="dialog animate-pop-in backdrop-animate-fade-in"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
method="dialog"
|
|
||||||
className="flex flex-col gap-4 p-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
fetchApi("/admins/:adminId", {
|
|
||||||
method: "DELETE",
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
params: { adminId: String(adminId) },
|
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
|
||||||
dialogRef.current?.close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Are you sure you want to delete this admin?
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2 items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-outlined"
|
|
||||||
onClick={() => dialogRef.current?.close()}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="button-filled">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
18
ui/App.tsx
18
ui/App.tsx
|
@ -1,28 +1,24 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import { AdminsPage } from "./AdminsPage.tsx";
|
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 { StatsPage } from "./StatsPage.tsx";
|
import { StatsPage } from "./StatsPage.tsx";
|
||||||
import { WorkersPage } from "./WorkersPage.tsx";
|
import { WorkersPage } from "./WorkersPage.tsx";
|
||||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
import { useLocalStorage } from "./useLocalStorage.ts";
|
|
||||||
import { DisclaimerPage } from "./DisclaimerPage.tsx";
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
// store session ID in the local storage
|
// store session ID in the local storage
|
||||||
const [sessionId, setSessionId] = useLocalStorage("sessionId");
|
const [sessionId, setSessionId] = useLocalStorage("sessionId", "");
|
||||||
|
|
||||||
// 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) {
|
||||||
fetchApi("/sessions", { method: "POST" }).then((resp) => resp).then(handleResponse).then(
|
fetchApi("sessions", "POST", {}).then(handleResponse).then((session) => {
|
||||||
(session) => {
|
|
||||||
console.log("Initialized session", session.id);
|
console.log("Initialized session", session.id);
|
||||||
setSessionId(session.id);
|
setSessionId(session.id);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
|
@ -31,16 +27,14 @@ export function App() {
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className="self-stretch"
|
className="self-stretch"
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
onLogOut={() => setSessionId(null)}
|
onLogOut={() => setSessionId("")}
|
||||||
/>
|
/>
|
||||||
<div className="self-center w-full max-w-screen-md flex flex-col items-stretch gap-4 p-4">
|
<div className="self-center w-full max-w-screen-md flex flex-col items-stretch gap-4 p-4">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<StatsPage />} />
|
<Route path="/" element={<StatsPage />} />
|
||||||
<Route path="/admins" element={<AdminsPage sessionId={sessionId} />} />
|
|
||||||
<Route path="/workers" element={<WorkersPage sessionId={sessionId} />} />
|
<Route path="/workers" element={<WorkersPage sessionId={sessionId} />} />
|
||||||
<Route path="/queue" element={<QueuePage />} />
|
<Route path="/queue" element={<QueuePage />} />
|
||||||
<Route path="/settings" element={<SettingsPage sessionId={sessionId} />} />
|
<Route path="/settings" element={<SettingsPage sessionId={sessionId} />} />
|
||||||
<Route path="/disclaimer" element={<DisclaimerPage />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
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 { cx } from "twind/core";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
import { API_URL, fetchApi, handleResponse } from "./apiClient.ts";
|
|
||||||
|
|
||||||
function NavTab(props: { to: string; children: ReactNode }) {
|
function NavTab(props: { to: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -15,50 +15,35 @@ function NavTab(props: { to: string; children: ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppHeader(props: {
|
export function AppHeader(
|
||||||
className?: string;
|
props: { className?: string; sessionId?: string; onLogOut: () => void },
|
||||||
sessionId: string | null;
|
) {
|
||||||
onLogOut: () => void;
|
|
||||||
}) {
|
|
||||||
const { className, sessionId, onLogOut } = props;
|
const { className, sessionId, onLogOut } = props;
|
||||||
|
|
||||||
const getSession = useSWR(
|
const session = useSWR(
|
||||||
sessionId ? ["/sessions/:sessionId", { method: "GET", params: { sessionId } }] as const : null,
|
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ onError: () => onLogOut() },
|
{ onError: () => onLogOut() },
|
||||||
);
|
);
|
||||||
|
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["/users/:userId", {
|
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
|
||||||
method: "GET",
|
|
||||||
params: { userId: String(getSession.data.userId) },
|
|
||||||
}] as const
|
|
||||||
: null,
|
: null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getBot = useSWR(
|
const bot = useSWR(
|
||||||
["/bot", { method: "GET" }] as const,
|
['bot',"GET",{}] as const, (args) => fetchApi(...args).then(handleResponse),
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getUserPhoto = useSWR(
|
const userPhoto = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["/users/:userId/photo", {
|
? ["users/{userId}/photo", "GET", {
|
||||||
method: "GET",
|
params: { userId: String(session.data.userId) },
|
||||||
params: { userId: String(getSession.data.userId) },
|
|
||||||
}] as const
|
}] as const
|
||||||
: null,
|
: null,
|
||||||
() =>
|
(args) => fetchApi(...args).then(handleResponse).then((blob) => URL.createObjectURL(blob)),
|
||||||
// elysia fetch can't download file
|
|
||||||
fetch(`${API_URL}/users/${getSession.data?.userId}/photo`)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.then((response) => response.blob())
|
|
||||||
.then((blob) => blob ? URL.createObjectURL(blob) : null),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -73,38 +58,44 @@ export function AppHeader(props: {
|
||||||
|
|
||||||
{/* tabs */}
|
{/* tabs */}
|
||||||
<nav className="flex-grow self-stretch flex items-stretch justify-center gap-2">
|
<nav className="flex-grow self-stretch flex items-stretch justify-center gap-2">
|
||||||
<NavTab to="/">Stats</NavTab>
|
<NavTab to="/">
|
||||||
<NavTab to="/admins">Admins</NavTab>
|
Stats
|
||||||
<NavTab to="/workers">Workers</NavTab>
|
</NavTab>
|
||||||
<NavTab to="/queue">Queue</NavTab>
|
<NavTab to="/workers">
|
||||||
<NavTab to="/settings">Settings</NavTab>
|
Workers
|
||||||
<NavTab to="/disclaimer">Disclaimer</NavTab>
|
</NavTab>
|
||||||
|
<NavTab to="/queue">
|
||||||
|
Queue
|
||||||
|
</NavTab>
|
||||||
|
<NavTab to="/settings">
|
||||||
|
Settings
|
||||||
|
</NavTab>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* loading indicator */}
|
{/* loading indicator */}
|
||||||
{getSession.isLoading || getUser.isLoading ? <div className="spinner" /> : null}
|
{session.isLoading || user.isLoading ? <div className="spinner" /> : null}
|
||||||
|
|
||||||
{/* user avatar */}
|
{/* user avatar */}
|
||||||
{getUser.data
|
{user.data
|
||||||
? getUserPhoto.data
|
? userPhoto.data
|
||||||
? (
|
? (
|
||||||
<img
|
<img
|
||||||
src={getUserPhoto.data}
|
src={userPhoto.data}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
className="w-9 h-9 rounded-full"
|
className="w-9 h-9 rounded-full"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className="w-9 h-9 rounded-full bg-zinc-400 dark:bg-zinc-500 flex items-center justify-center text-white text-2xl font-bold select-none">
|
<div className="w-9 h-9 rounded-full bg-zinc-400 dark:bg-zinc-500 flex items-center justify-center text-white text-2xl font-bold select-none">
|
||||||
{getUser.data.first_name.at(0)?.toUpperCase()}
|
{user.data.first_name.at(0)?.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{/* login/logout button */}
|
{/* login/logout button */}
|
||||||
{!getSession.isLoading && !getUser.isLoading && getBot.data && sessionId
|
{!session.isLoading && !user.isLoading && bot.data && sessionId
|
||||||
? (
|
? (
|
||||||
getUser.data
|
user.data
|
||||||
? (
|
? (
|
||||||
<button className="button-outlined" onClick={() => onLogOut()}>
|
<button className="button-outlined" onClick={() => onLogOut()}>
|
||||||
Logout
|
Logout
|
||||||
|
@ -113,7 +104,7 @@ export function AppHeader(props: {
|
||||||
: (
|
: (
|
||||||
<a
|
<a
|
||||||
className="button-filled"
|
className="button-filled"
|
||||||
href={`https://t.me/${getBot.data.username}?start=${sessionId}`}
|
href={`https://t.me/${bot.data.username}?start=${sessionId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { cx } from "@twind/core";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { cx } from "twind/core";
|
|
||||||
|
|
||||||
function CounterDigit(props: { value: number; transitionDurationMs?: number | undefined }) {
|
function CounterDigit(props: { value: number; transitionDurationMs?: number }) {
|
||||||
const { value, transitionDurationMs = 1500 } = props;
|
const { value, transitionDurationMs = 1500 } = props;
|
||||||
const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1;
|
const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1;
|
||||||
|
|
||||||
|
@ -36,10 +36,10 @@ const CounterText = (props: { children: React.ReactNode }) => (
|
||||||
export function Counter(props: {
|
export function Counter(props: {
|
||||||
value: number;
|
value: number;
|
||||||
digits: number;
|
digits: number;
|
||||||
fractionDigits?: number | undefined;
|
fractionDigits?: number;
|
||||||
transitionDurationMs?: number | undefined;
|
transitionDurationMs?: number;
|
||||||
className?: string | undefined;
|
className?: string;
|
||||||
postfix?: string | undefined;
|
postfix?: string;
|
||||||
}) {
|
}) {
|
||||||
const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props;
|
const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props;
|
||||||
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function DisclaimerPage() {
|
|
||||||
return (
|
|
||||||
<div className="my-16 flex flex-col gap-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
<h2 className="text-center text-3xl font-bold">Service Agreement and Disclaimer</h2>
|
|
||||||
|
|
||||||
{ /* Each section is now enclosed in a styled block */ }
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">About the Service</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
This website and <a href="https://t.me/NyxTheBot" className="text-blue-600 hover:text-blue-800">@NyxTheBot</a> are privately hosted services, operated as a non-commercial hobby project. They offer a service for generating images from text inputs using the Stable Diffusion model via a Telegram bot <a href="https://t.me/NyxTheBot" className="text-blue-600 hover:text-blue-800">@NyxTheBot</a>. This service is provided free of charge, without any commercial intent, and relies solely on voluntary tips for its operating costs. You can support this project with a tip at <a href="https://ko-fi.com/nyxthebot" className="text-blue-600 hover:text-blue-800">ko-fi.com/nyxthebot</a>.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">Disclaimer of Warranties and Limitation of Liability</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
THIS WEBSITE AND ASSOCIATED SERVICES ARE PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND SERVICE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">Open Source</h3>
|
|
||||||
<p className="text-xl mb-2">
|
|
||||||
This service is open source. The source code can be explored and utilized for your own projects.
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc pl-6">
|
|
||||||
<li className="text-xl">
|
|
||||||
Find the source code at <a href="https://git.foxo.me/Akiru/nyx" className="text-blue-600 hover:text-blue-800">git.foxo.me/Akiru/nyx</a>
|
|
||||||
</li>
|
|
||||||
<li className="text-xl">
|
|
||||||
Originally forked from <a href="https://git.foxo.me/pinks/eris" className="text-blue-600 hover:text-blue-800">git.foxo.me/pinks/eris</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">User Responsibilities</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
Users are solely responsible for the inputs they provide and the resulting outputs. Users must ensure that their use of the service complies with all applicable laws and regulations. The service should not be used for generating content that is considered illegal or harmful in applicable jurisdictions or interpretations of law. Content generated by this service is fictitious, generated by a randomized computing process, and any similarity to existing persons, objects, entities or situations is unintentional, regardless of input by the user.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">Data Handling and Privacy</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
In the course of using this service, specific data may be collected and stored. This includes associating your Telegram username with the prompts you provide to the bot and the resulting images generated. Our collection and use of this data is solely for the purpose of providing and improving the service. We are committed to handling your data responsibly and safely.
|
|
||||||
</p>
|
|
||||||
<p className="text-xl">
|
|
||||||
Please be aware that your interactions with the bot, including the prompts you submit and the images generated, are logged and may be stored for service optimization, troubleshooting, and enhancing user experience. We endeavor to protect the privacy and integrity of your data, and ensure that it is used only for the intended purposes of this service. However, since the service integrates with third parties (i.e. Telegram and potentially services local to you for data transport and storage), your use will also be subject to those third parties' data handling and privacy policies that are not within our scope.
|
|
||||||
</p>
|
|
||||||
<p className="text-xl">
|
|
||||||
We respect your privacy and are dedicated to safeguarding your personal information. If you have any concerns or questions about how your data is handled, please feel free to contact us.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">External Links</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
This website may contain links to external websites that are not operated by us. We have no control over the content and practices of these sites and cannot accept responsibility or liability for their respective privacy policies.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
|
||||||
<h3 className="text-2xl font-semibold mb-4">Amendments to the Agreement</h3>
|
|
||||||
<p className="text-xl">
|
|
||||||
The host reserves the right to make changes to this service agreement at any time without notice.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { cx } from "@twind/core";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { cx } from "twind/core";
|
|
||||||
|
|
||||||
export function Progress(props: { value: number; className?: string }) {
|
export function Progress(props: { value: number; className?: string }) {
|
||||||
const { value, className } = props;
|
const { value, className } = props;
|
||||||
|
|
|
@ -3,11 +3,11 @@ 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 { fetchApi, handleResponse } from "./apiClient.ts";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
|
|
||||||
export function QueuePage() {
|
export function QueuePage() {
|
||||||
const getJobs = useSWR(
|
const jobs = useSWR(
|
||||||
["/jobs", {}] as const,
|
["jobs", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 2000 },
|
{ refreshInterval: 2000 },
|
||||||
);
|
);
|
||||||
|
@ -15,43 +15,36 @@ export function QueuePage() {
|
||||||
return (
|
return (
|
||||||
<FlipMove
|
<FlipMove
|
||||||
typeName={"ul"}
|
typeName={"ul"}
|
||||||
className="flex flex-col items-stretch gap-2 p-2 bg-zinc-200 dark:bg-zinc-800 rounded-md"
|
className="flex flex-col items-stretch gap-2 p-2 bg-zinc-200 dark:bg-zinc-800 rounded-xl"
|
||||||
enterAnimation="fade"
|
enterAnimation="fade"
|
||||||
leaveAnimation="fade"
|
leaveAnimation="fade"
|
||||||
>
|
>
|
||||||
{getJobs.data && getJobs.data.length === 0
|
{jobs.data?.map((job) => (
|
||||||
? <li key="no-jobs" className="text-center text-gray-500">Queue is empty.</li>
|
|
||||||
: (
|
|
||||||
getJobs.data?.map((job) => (
|
|
||||||
<li
|
<li
|
||||||
className="flex items-baseline gap-2 bg-zinc-100 dark:bg-zinc-700 px-2 py-1 rounded-md"
|
className="flex items-baseline gap-2 bg-zinc-100 dark:bg-zinc-700 px-2 py-1 rounded-xl"
|
||||||
key={job.id}
|
key={job.id.join("/")}
|
||||||
>
|
>
|
||||||
<span className="">{job.place}.</span>
|
<span className="">
|
||||||
<span>{getFlagEmoji(job.state.from.language_code ?? undefined)}</span>
|
{job.place}.
|
||||||
|
</span>
|
||||||
|
<span>{getFlagEmoji(job.state.from.language_code)}</span>
|
||||||
<strong>{job.state.from.first_name} {job.state.from.last_name}</strong>
|
<strong>{job.state.from.first_name} {job.state.from.last_name}</strong>
|
||||||
{job.state.from.username
|
{job.state.from.username
|
||||||
? (
|
? (
|
||||||
<a
|
<a className="link" href={`https://t.me/${job.state.from.username}`} target="_blank">
|
||||||
className="link"
|
|
||||||
href={`https://t.me/${job.state.from.username}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
@{job.state.from.username}
|
@{job.state.from.username}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
<span className="flex-grow self-center h-full">
|
<span className="flex-grow self-center h-full">
|
||||||
{job.state.progress != null && (
|
{job.state.progress != null &&
|
||||||
<Progress className="w-full h-full" value={job.state.progress} />
|
<Progress className="w-full h-full" value={job.state.progress} />}
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-zinc-500 dark:text-zinc-400">
|
<span className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
{job.state.workerInstanceKey}
|
{job.state.workerInstanceKey}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</FlipMove>
|
</FlipMove>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +1,79 @@
|
||||||
import React, { useState } from "react";
|
import { cx } from "@twind/core";
|
||||||
|
import React, { ReactNode, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { cx } from "twind/core";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
|
||||||
import { omitUndef } from "../utils/omitUndef.ts";
|
|
||||||
|
|
||||||
export function SettingsPage(props: { sessionId: string | null }) {
|
export function SettingsPage(props: { sessionId: string }) {
|
||||||
const { sessionId } = props;
|
const { sessionId } = props;
|
||||||
|
const session = useSWR(
|
||||||
const getSession = useSWR(
|
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
|
||||||
sessionId ? ["/sessions/:sessionId", { params: { sessionId } }] as const : null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["/users/:userId", { params: { userId: String( getSession.data.userId) } }] as const
|
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
|
||||||
: null,
|
: null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
const getParams = useSWR(
|
const params = useSWR(
|
||||||
["/settings/params", {}] as const,
|
["settings/params", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
const [newParams, setNewParams] = useState<Partial<typeof getParams.data>>({});
|
const [changedParams, setChangedParams] = useState<Partial<typeof params.data>>({});
|
||||||
const [patchParamsError, setPatchParamsError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="flex flex-col items-stretch gap-4"
|
className="flex flex-col items-stretch gap-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
getParams.mutate(() =>
|
params.mutate(() =>
|
||||||
fetchApi("/settings/params", {
|
fetchApi("settings/params", "PATCH", {
|
||||||
method: "PATCH",
|
query: { sessionId },
|
||||||
query: { sessionId: sessionId ?? "" },
|
body: { type: "application/json", data: changedParams ?? {} },
|
||||||
body: omitUndef(newParams ?? {}),
|
|
||||||
}).then(handleResponse)
|
}).then(handleResponse)
|
||||||
)
|
)
|
||||||
.then(() => setNewParams({}))
|
.then(() => setChangedParams({}))
|
||||||
.catch((e) => setPatchParamsError(String(e)));
|
.catch((e) => setError(String(e)));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
<label className="flex flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Negative prompt {newParams?.negative_prompt != null ? "(Changed)" : ""}
|
Negative prompt {changedParams?.negative_prompt != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
className="input-text"
|
className="input-text"
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.negative_prompt ??
|
value={changedParams?.negative_prompt ??
|
||||||
getParams.data?.negative_prompt ??
|
params.data?.negative_prompt ??
|
||||||
""}
|
""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
negative_prompt: e.target.value,
|
negative_prompt: e.target.value,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
<label className="flex flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Sampler {newParams?.sampler_name != null ? "(Changed)" : ""}
|
Sampler {changedParams?.sampler_name != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="input-text"
|
className="input-text"
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.sampler_name ??
|
value={changedParams?.sampler_name ??
|
||||||
getParams.data?.sampler_name ??
|
params.data?.sampler_name ??
|
||||||
""}
|
""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
sampler_name: e.target.value,
|
sampler_name: e.target.value,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
<label className="flex flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Steps {newParams?.steps != null ? "(Changed)" : ""}
|
Steps {changedParams?.steps != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
|
@ -87,12 +82,12 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={5}
|
min={5}
|
||||||
max={50}
|
max={50}
|
||||||
step={5}
|
step={5}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.steps ??
|
value={changedParams?.steps ??
|
||||||
getParams.data?.steps ??
|
params.data?.steps ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
steps: Number(e.target.value),
|
steps: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
|
@ -103,22 +98,21 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={5}
|
min={5}
|
||||||
max={50}
|
max={50}
|
||||||
step={5}
|
step={5}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.steps ??
|
value={changedParams?.steps ??
|
||||||
getParams.data?.steps ??
|
params.data?.steps ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
steps: Number(e.target.value),
|
steps: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
<label className="flex flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Detail {newParams?.cfg_scale != null ? "(Changed)" : ""}
|
Detail {changedParams?.cfg_scale != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
|
@ -127,12 +121,12 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
step={1}
|
step={1}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.cfg_scale ??
|
value={changedParams?.cfg_scale ??
|
||||||
getParams.data?.cfg_scale ??
|
params.data?.cfg_scale ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
cfg_scale: Number(e.target.value),
|
cfg_scale: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
|
@ -143,23 +137,22 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
step={1}
|
step={1}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.cfg_scale ??
|
value={changedParams?.cfg_scale ??
|
||||||
getParams.data?.cfg_scale ??
|
params.data?.cfg_scale ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
cfg_scale: Number(e.target.value),
|
cfg_scale: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Width {newParams?.width != null ? "(Changed)" : ""}
|
Width {changedParams?.width != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="input-text"
|
className="input-text"
|
||||||
|
@ -167,12 +160,12 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={64}
|
min={64}
|
||||||
max={2048}
|
max={2048}
|
||||||
step={64}
|
step={64}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.width ??
|
value={changedParams?.width ??
|
||||||
getParams.data?.width ??
|
params.data?.width ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
width: Number(e.target.value),
|
width: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
|
@ -183,12 +176,12 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={64}
|
min={64}
|
||||||
max={2048}
|
max={2048}
|
||||||
step={64}
|
step={64}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.width ??
|
value={changedParams?.width ??
|
||||||
getParams.data?.width ??
|
params.data?.width ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
width: Number(e.target.value),
|
width: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
|
@ -196,7 +189,7 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
</label>
|
</label>
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Height {newParams?.height != null ? "(Changed)" : ""}
|
Height {changedParams?.height != null ? "(Changed)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="input-text"
|
className="input-text"
|
||||||
|
@ -204,12 +197,12 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={64}
|
min={64}
|
||||||
max={2048}
|
max={2048}
|
||||||
step={64}
|
step={64}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.height ??
|
value={changedParams?.height ??
|
||||||
getParams.data?.height ??
|
params.data?.height ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
height: Number(e.target.value),
|
height: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
|
@ -220,54 +213,35 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
min={64}
|
min={64}
|
||||||
max={2048}
|
max={2048}
|
||||||
step={64}
|
step={64}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin}
|
disabled={params.isLoading || !user.data?.isAdmin}
|
||||||
value={newParams?.height ??
|
value={changedParams?.height ??
|
||||||
getParams.data?.height ??
|
params.data?.height ??
|
||||||
0}
|
0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewParams((params) => ({
|
setChangedParams((params) => ({
|
||||||
...params,
|
...params,
|
||||||
height: Number(e.target.value),
|
height: Number(e.target.value),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{error ? <Alert onClose={() => setError(undefined)}>{error}</Alert> : null}
|
||||||
{patchParamsError
|
{params.error ? <Alert>{params.error.message}</Alert> : null}
|
||||||
? (
|
|
||||||
<p className="alert">
|
|
||||||
<span className="flex-grow">Updating params failed: {patchParamsError}</span>
|
|
||||||
<button className="button-ghost" onClick={() => setPatchParamsError(undefined)}>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{getParams.error
|
|
||||||
? (
|
|
||||||
<p className="alert">
|
|
||||||
<span className="flex-grow">
|
|
||||||
Loading params failed: {String(getParams.error)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center justify-end">
|
<div className="flex gap-2 items-center justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cx("button-outlined ripple", getParams.isLoading && "bg-stripes")}
|
className={cx("button-outlined ripple", params.isLoading && "bg-stripes")}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin ||
|
disabled={params.isLoading || !user.data?.isAdmin ||
|
||||||
Object.keys(newParams ?? {}).length === 0}
|
Object.keys(changedParams ?? {}).length === 0}
|
||||||
onClick={() => setNewParams({})}
|
onClick={() => setChangedParams({})}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={cx("button-filled ripple", getParams.isLoading && "bg-stripes")}
|
className={cx("button-filled ripple", params.isLoading && "bg-stripes")}
|
||||||
disabled={getParams.isLoading || !getUser.data?.admin ||
|
disabled={params.isLoading || !user.data?.isAdmin ||
|
||||||
Object.keys(newParams ?? {}).length === 0}
|
Object.keys(changedParams ?? {}).length === 0}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
@ -275,3 +249,26 @@ export function SettingsPage(props: { sessionId: string | null }) {
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Alert(props: { children: ReactNode; onClose?: () => void }) {
|
||||||
|
const { children, onClose } = props;
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
role="alert"
|
||||||
|
className="px-4 py-2 flex gap-2 items-center bg-red-500 text-white rounded-sm shadow-md"
|
||||||
|
>
|
||||||
|
<span className="flex-grow">{children}</span>
|
||||||
|
{onClose
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-ghost"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Counter } from "./Counter.tsx";
|
import { Counter } from "./Counter.tsx";
|
||||||
|
|
||||||
export function StatsPage() {
|
export function StatsPage() {
|
||||||
const getGlobalStats = useSWR(
|
const globalStats = useSWR(
|
||||||
["/stats", {}] as const,
|
["stats", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 1_00 },
|
{ refreshInterval: 2_000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,15 +16,15 @@ export function StatsPage() {
|
||||||
<span>Pixelsteps diffused</span>
|
<span>Pixelsteps diffused</span>
|
||||||
<Counter
|
<Counter
|
||||||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||||
value={getGlobalStats.data?.pixelStepCount ?? 0}
|
value={globalStats.data?.pixelStepCount ?? 0}
|
||||||
digits={15}
|
digits={15}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={3_000}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
className="text-base"
|
className="text-base"
|
||||||
value={(getGlobalStats.data?.pixelStepsPerMinute ?? 0) / 60}
|
value={(globalStats.data?.pixelStepsPerMinute ?? 0) / 60}
|
||||||
digits={9}
|
digits={9}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -32,15 +32,15 @@ export function StatsPage() {
|
||||||
<span>Pixels painted</span>
|
<span>Pixels painted</span>
|
||||||
<Counter
|
<Counter
|
||||||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||||
value={getGlobalStats.data?.pixelCount ?? 0}
|
value={globalStats.data?.pixelCount ?? 0}
|
||||||
digits={15}
|
digits={15}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={3_000}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
className="text-base"
|
className="text-base"
|
||||||
value={(getGlobalStats.data?.pixelsPerMinute ?? 0) / 60}
|
value={(globalStats.data?.pixelsPerMinute ?? 0) / 60}
|
||||||
digits={9}
|
digits={9}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -49,16 +49,16 @@ export function StatsPage() {
|
||||||
<span>Steps processed</span>
|
<span>Steps processed</span>
|
||||||
<Counter
|
<Counter
|
||||||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||||
value={getGlobalStats.data?.stepCount ?? 0}
|
value={globalStats.data?.stepCount ?? 0}
|
||||||
digits={9}
|
digits={9}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={3_000}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
className="text-base"
|
className="text-base"
|
||||||
value={(getGlobalStats.data?.stepsPerMinute ?? 0) / 60}
|
value={(globalStats.data?.stepsPerMinute ?? 0) / 60}
|
||||||
digits={3}
|
digits={3}
|
||||||
fractionDigits={3}
|
fractionDigits={3}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -66,16 +66,16 @@ export function StatsPage() {
|
||||||
<span>Images generated</span>
|
<span>Images generated</span>
|
||||||
<Counter
|
<Counter
|
||||||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||||
value={getGlobalStats.data?.imageCount ?? 0}
|
value={globalStats.data?.imageCount ?? 0}
|
||||||
digits={9}
|
digits={9}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={3_000}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
className="text-base"
|
className="text-base"
|
||||||
value={(getGlobalStats.data?.imagesPerMinute ?? 0) / 60}
|
value={(globalStats.data?.imagesPerMinute ?? 0) / 60}
|
||||||
digits={3}
|
digits={3}
|
||||||
fractionDigits={3}
|
fractionDigits={3}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -84,9 +84,9 @@ export function StatsPage() {
|
||||||
<span>Unique users</span>
|
<span>Unique users</span>
|
||||||
<Counter
|
<Counter
|
||||||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||||
value={getGlobalStats.data?.userCount ?? 0}
|
value={globalStats.data?.userCount ?? 0}
|
||||||
digits={6}
|
digits={6}
|
||||||
transitionDurationMs={1_00}
|
transitionDurationMs={1_500}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,81 +1,51 @@
|
||||||
import React, { RefObject, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { FormattedRelativeTime } from "react-intl";
|
import { FormattedRelativeTime } from "react-intl";
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
import { WorkerResponse } from "../api/workersRoute.ts";
|
import { WorkerData } from "../api/workersRoute.ts";
|
||||||
import { Counter } from "./Counter.tsx";
|
import { Counter } from "./Counter.tsx";
|
||||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
import { fetchApi, handleResponse } from "./apiClient.tsx";
|
||||||
|
|
||||||
export function WorkersPage(props: { sessionId: string | null }) {
|
export function WorkersPage(props: { sessionId?: string }) {
|
||||||
const { sessionId } = props;
|
const { sessionId } = props;
|
||||||
|
|
||||||
const addDialogRef = useRef<HTMLDialogElement>(null);
|
const createWorkerModalRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
const getSession = useSWR(
|
const session = useSWR(
|
||||||
sessionId ? ["/sessions/:sessionId", { params: { sessionId } }] as const : null,
|
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["/users/:userId", { params: { userId: String(getSession.data.userId) } }] as const
|
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
|
||||||
: null,
|
: null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getWorkers = useSWR(
|
const workers = useSWR(
|
||||||
["/workers", { method: "GET" }] as const,
|
["workers", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 5000 },
|
{ refreshInterval: 5000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getWorkers.data?.length
|
<ul className="my-4 flex flex-col gap-2">
|
||||||
? (
|
{workers.data?.map((worker) => (
|
||||||
<ul className="flex flex-col gap-2">
|
|
||||||
{getWorkers.data?.map((worker) => (
|
|
||||||
<WorkerListItem key={worker.id} worker={worker} sessionId={sessionId} />
|
<WorkerListItem key={worker.id} worker={worker} sessionId={sessionId} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
{user.data?.isAdmin && (
|
||||||
: getWorkers.data?.length === 0
|
|
||||||
? (
|
|
||||||
<ul className="flex flex-col gap-2">
|
|
||||||
<li className="flex flex-col gap-2 rounded-md bg-zinc-100 dark:bg-zinc-800 p-2">
|
|
||||||
<p key="no-workers" className="text-center text-gray-500">No workers.</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
: getWorkers.error
|
|
||||||
? <p className="alert">Loading workers failed</p>
|
|
||||||
: <div className="spinner self-center" />}
|
|
||||||
|
|
||||||
{getUser.data?.admin && (
|
|
||||||
<button
|
<button
|
||||||
className="button-filled"
|
className="button-filled"
|
||||||
onClick={() => addDialogRef.current?.showModal()}
|
onClick={() => createWorkerModalRef.current?.showModal()}
|
||||||
>
|
>
|
||||||
Add worker
|
Add worker
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<dialog
|
||||||
<AddWorkerDialog
|
className="dialog"
|
||||||
dialogRef={addDialogRef}
|
ref={createWorkerModalRef}
|
||||||
sessionId={sessionId}
|
>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddWorkerDialog(props: {
|
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, sessionId } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog ref={dialogRef} className="dialog animate-pop-in backdrop-animate-fade-in">
|
|
||||||
<form
|
<form
|
||||||
method="dialog"
|
method="dialog"
|
||||||
className="flex flex-col gap-4 p-4"
|
className="flex flex-col gap-4 p-4"
|
||||||
|
@ -88,17 +58,23 @@ function AddWorkerDialog(props: {
|
||||||
const user = data.get("user") as string;
|
const user = data.get("user") as string;
|
||||||
const password = data.get("password") as string;
|
const password = data.get("password") as string;
|
||||||
console.log(key, name, user, password);
|
console.log(key, name, user, password);
|
||||||
fetchApi("/workers", {
|
workers.mutate(async () => {
|
||||||
method: "POST",
|
const worker = await fetchApi("workers", "POST", {
|
||||||
query: { sessionId: sessionId! },
|
query: { sessionId: sessionId! },
|
||||||
body: {
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
key,
|
key,
|
||||||
name: name || null,
|
name: name || null,
|
||||||
sdUrl,
|
sdUrl,
|
||||||
sdAuth: user && password ? { user, password } : null,
|
sdAuth: user && password ? { user, password } : null,
|
||||||
},
|
},
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
},
|
||||||
dialogRef.current?.close();
|
}).then(handleResponse);
|
||||||
|
return [...(workers.data ?? []), worker];
|
||||||
|
});
|
||||||
|
|
||||||
|
createWorkerModalRef.current?.close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
@ -169,7 +145,7 @@ function AddWorkerDialog(props: {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button-outlined"
|
className="button-outlined"
|
||||||
onClick={() => dialogRef.current?.close()}
|
onClick={() => createWorkerModalRef.current?.close()}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
@ -179,28 +155,28 @@ function AddWorkerDialog(props: {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkerListItem(props: {
|
function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
|
||||||
worker: WorkerResponse;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { worker, sessionId } = props;
|
const { worker, sessionId } = props;
|
||||||
const editDialogRef = useRef<HTMLDialogElement>(null);
|
const editWorkerModalRef = useRef<HTMLDialogElement>(null);
|
||||||
const deleteDialogRef = useRef<HTMLDialogElement>(null);
|
const deleteWorkerModalRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
const getSession = useSWR(
|
const session = useSWR(
|
||||||
sessionId ? ["/sessions/:sessionId", { params: { sessionId } }] as const : null,
|
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["/users/:userId", { params: { userId: String(getSession.data.userId) } }] as const
|
? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const
|
||||||
: null,
|
: null,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex flex-col gap-2 rounded-md bg-zinc-100 dark:bg-zinc-800 p-2">
|
<li className="flex flex-col gap-2 rounded-md bg-zinc-100 dark:bg-zinc-800 p-2">
|
||||||
<p className="font-bold">
|
<p className="font-bold">
|
||||||
|
@ -236,49 +212,27 @@ function WorkerListItem(props: {
|
||||||
{" "}
|
{" "}
|
||||||
<Counter value={worker.stepsPerMinute} digits={3} /> steps per minute
|
<Counter value={worker.stepsPerMinute} digits={3} /> steps per minute
|
||||||
</p>
|
</p>
|
||||||
{getUser.data?.admin && (
|
|
||||||
<p className="flex gap-2">
|
<p className="flex gap-2">
|
||||||
|
{user.data?.isAdmin && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
className="button-outlined"
|
className="button-outlined"
|
||||||
onClick={() => editDialogRef.current?.showModal()}
|
onClick={() => editWorkerModalRef.current?.showModal()}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="button-outlined"
|
className="button-outlined"
|
||||||
onClick={() => deleteDialogRef.current?.showModal()}
|
onClick={() => deleteWorkerModalRef.current?.showModal()}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</>
|
||||||
)}
|
)}
|
||||||
<EditWorkerDialog
|
</p>
|
||||||
dialogRef={editDialogRef}
|
|
||||||
workerId={worker.id}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
<DeleteWorkerDialog
|
|
||||||
dialogRef={deleteDialogRef}
|
|
||||||
workerId={worker.id}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditWorkerDialog(props: {
|
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
|
||||||
workerId: string;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, workerId, sessionId } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog
|
<dialog
|
||||||
className="dialog animate-pop-in backdrop-animate-fade-in"
|
className="dialog"
|
||||||
ref={dialogRef}
|
ref={editWorkerModalRef}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
method="dialog"
|
method="dialog"
|
||||||
|
@ -289,15 +243,17 @@ function EditWorkerDialog(props: {
|
||||||
const user = data.get("user") as string;
|
const user = data.get("user") as string;
|
||||||
const password = data.get("password") as string;
|
const password = data.get("password") as string;
|
||||||
console.log(user, password);
|
console.log(user, password);
|
||||||
fetchApi("/workers/:workerId", {
|
fetchApi("workers/{workerId}", "PATCH", {
|
||||||
method: "PATCH",
|
params: { workerId: worker.id },
|
||||||
params: { workerId: workerId },
|
|
||||||
query: { sessionId: sessionId! },
|
query: { sessionId: sessionId! },
|
||||||
body: {
|
body: {
|
||||||
sdAuth: user && password ? { user, password } : null,
|
type: "application/json",
|
||||||
|
data: {
|
||||||
|
auth: user && password ? { user, password } : null,
|
||||||
},
|
},
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
},
|
||||||
dialogRef.current?.close();
|
});
|
||||||
|
editWorkerModalRef.current?.close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
@ -326,7 +282,7 @@ function EditWorkerDialog(props: {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button-outlined"
|
className="button-outlined"
|
||||||
onClick={() => dialogRef.current?.close()}
|
onClick={() => editWorkerModalRef.current?.close()}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
@ -336,33 +292,20 @@ function EditWorkerDialog(props: {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteWorkerDialog(props: {
|
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
|
||||||
workerId: string;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, workerId, sessionId } = props;
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog
|
<dialog
|
||||||
className="dialog animate-pop-in backdrop-animate-fade-in"
|
className="dialog"
|
||||||
ref={dialogRef}
|
ref={deleteWorkerModalRef}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
method="dialog"
|
method="dialog"
|
||||||
className="flex flex-col gap-4 p-4"
|
className="flex flex-col gap-4 p-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
fetchApi("/workers/:workerId", {
|
fetchApi("workers/{workerId}", "DELETE", {
|
||||||
method: "DELETE",
|
params: { workerId: worker.id },
|
||||||
params: { workerId: workerId },
|
|
||||||
query: { sessionId: sessionId! },
|
query: { sessionId: sessionId! },
|
||||||
}).then(handleResponse).then(() => mutate(() => true));
|
}).then(handleResponse).then(() => mutate(["workers", "GET", {}]));
|
||||||
dialogRef.current?.close();
|
deleteWorkerModalRef.current?.close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
@ -372,7 +315,7 @@ function DeleteWorkerDialog(props: {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button-outlined"
|
className="button-outlined"
|
||||||
onClick={() => dialogRef.current?.close()}
|
onClick={() => deleteWorkerModalRef.current?.close()}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
@ -382,5 +325,6 @@ function DeleteWorkerDialog(props: {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { edenFetch } from "elysia/eden";
|
|
||||||
import { Api } from "../api/serveApi.ts";
|
|
||||||
|
|
||||||
export const API_URL = "/api";
|
|
||||||
|
|
||||||
export const fetchApi = edenFetch<Api>(API_URL);
|
|
||||||
|
|
||||||
export function handleResponse<
|
|
||||||
T extends
|
|
||||||
| { data: unknown; error: null }
|
|
||||||
| { data: null; error: { status: number; value: unknown } },
|
|
||||||
>(
|
|
||||||
response: T,
|
|
||||||
): (T & { error: null })["data"] {
|
|
||||||
if (response.error) {
|
|
||||||
throw new Error(`${response.error?.status}: ${response.error?.value}`);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { createFetcher, Output } from "t_rest/client";
|
||||||
|
import { ApiHandler } from "../api/serveApi.ts";
|
||||||
|
|
||||||
|
export const fetchApi = createFetcher<ApiHandler>({
|
||||||
|
baseUrl: `${location.origin}/api/`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function handleResponse<T extends Output>(
|
||||||
|
response: T,
|
||||||
|
): (T & { status: 200 })["body"]["data"] {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(String(response.body.data));
|
||||||
|
}
|
||||||
|
return response.body.data;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -5,7 +5,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="/favicon.png" type="image/png">
|
<link rel="icon" href="/favicon.png" type="image/png">
|
||||||
<script type="module" src="/main.tsx"></script>
|
<script type="module" src="/main.tsx"></script>
|
||||||
<script async src="/es-module-shims.js"></script>
|
|
||||||
<title>Eris</title>
|
<title>Eris</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
32
ui/twind.ts
32
ui/twind.ts
|
@ -1,12 +1,11 @@
|
||||||
import { defineConfig, injectGlobal, install } from "twind/core";
|
import { defineConfig, injectGlobal, install } from "@twind/core";
|
||||||
import presetTailwind from "twind/preset-tailwind";
|
import presetTailwind from "@twind/preset-tailwind";
|
||||||
|
|
||||||
const twConfig = defineConfig({
|
const twConfig = defineConfig({
|
||||||
presets: [presetTailwind()],
|
presets: [presetTailwind()],
|
||||||
hash: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
install(twConfig, false);
|
install(twConfig);
|
||||||
|
|
||||||
injectGlobal`
|
injectGlobal`
|
||||||
@layer base {
|
@layer base {
|
||||||
|
@ -28,11 +27,14 @@ injectGlobal`
|
||||||
rgba(0, 0, 0, 0.1) 14px,
|
rgba(0, 0, 0, 0.1) 14px,
|
||||||
rgba(0, 0, 0, 0.1) 28px
|
rgba(0, 0, 0, 0.1) 28px
|
||||||
);
|
);
|
||||||
animation: bg-scroll 0.5s linear infinite;
|
animation: bg-stripes-scroll 0.5s linear infinite;
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
}
|
}
|
||||||
@keyframes bg-scroll {
|
@keyframes bg-stripes-scroll {
|
||||||
to {
|
0% {
|
||||||
|
background-position: 0 40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
background-position: 40px 0;
|
background-position: 40px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,11 +63,11 @@ injectGlobal`
|
||||||
opacity 0s;
|
opacity 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop-animate-fade-in::backdrop {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.3s ease-out forwards;
|
animation: fade-in 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +76,7 @@ injectGlobal`
|
||||||
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
}
|
}
|
||||||
@keyframes pop-in {
|
@keyframes pop-in {
|
||||||
from {
|
0% {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -90,10 +92,6 @@ injectGlobal`
|
||||||
@apply h-8 w-8 animate-spin rounded-full border-4 border-transparent border-t-current;
|
@apply h-8 w-8 animate-spin rounded-full border-4 border-transparent border-t-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
@apply px-4 py-2 flex gap-2 items-center bg-red-500 text-white rounded-sm shadow-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-filled {
|
.button-filled {
|
||||||
@apply rounded-md bg-sky-600 px-3 py-2 min-h-12
|
@apply rounded-md bg-sky-600 px-3 py-2 min-h-12
|
||||||
text-sm font-semibold uppercase tracking-wider text-white shadow-sm transition-all
|
text-sm font-semibold uppercase tracking-wider text-white shadow-sm transition-all
|
||||||
|
@ -116,7 +114,7 @@ injectGlobal`
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@apply inline-flex items-center px-4 text-sm text-center bg-transparent border-b-2 sm:text-base whitespace-nowrap outline-none
|
@apply inline-flex items-center px-4 text-sm text-center bg-transparent border-b-2 sm:text-base whitespace-nowrap focus:outline-none
|
||||||
border-transparent dark:text-white cursor-base hover:border-zinc-400 focus:border-zinc-400 transition-all;
|
border-transparent dark:text-white cursor-base hover:border-zinc-400 focus:border-zinc-400 transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,8 +134,8 @@ injectGlobal`
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
@apply overflow-hidden overflow-y-auto rounded-md shadow-xl
|
@apply animate-pop-in backdrop:animate-fade-in overflow-hidden overflow-y-auto rounded-md
|
||||||
bg-white text-zinc-900 shadow-lg backdrop:bg-black/30 dark:bg-zinc-800 dark:text-zinc-100;
|
bg-zinc-100 text-zinc-900 shadow-lg backdrop:bg-black/20 dark:bg-zinc-800 dark:text-zinc-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function useLocalStorage(key: string) {
|
|
||||||
const [value, setValue] = useState(() => window.localStorage.getItem(key));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(key, value);
|
|
||||||
if (value != null) {
|
|
||||||
window.localStorage.setItem(key, value);
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
}, [key, value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleStorage = (event: StorageEvent) => {
|
|
||||||
if (event.key === key) {
|
|
||||||
console.log(key, event.newValue);
|
|
||||||
setValue(event.newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("storage", handleStorage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", handleStorage);
|
|
||||||
};
|
|
||||||
}, [key]);
|
|
||||||
|
|
||||||
return [value, setValue] as const;
|
|
||||||
}
|
|
76
utils/Tkv.ts
76
utils/Tkv.ts
|
@ -1,76 +0,0 @@
|
||||||
export type TkvEntry<K extends Deno.KvKey, T> = {
|
|
||||||
key: readonly [...K];
|
|
||||||
value: T;
|
|
||||||
versionstamp: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TkvEntryMaybe<K extends Deno.KvKey, T> = TkvEntry<K, T> | {
|
|
||||||
key: readonly [...K];
|
|
||||||
value: null;
|
|
||||||
versionstamp: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TkvListSelector<K extends Deno.KvKey> =
|
|
||||||
| { prefix: KvKeyPrefix<K> }
|
|
||||||
| { prefix: KvKeyPrefix<K>; start: readonly [...K] }
|
|
||||||
| { prefix: KvKeyPrefix<K>; end: readonly [...K] }
|
|
||||||
| { start: readonly [...K]; end: readonly [...K] };
|
|
||||||
|
|
||||||
export type KvKeyPrefix<Key extends Deno.KvKey> = Key extends readonly [infer Prefix, ...infer Rest]
|
|
||||||
? readonly [Prefix] | readonly [Prefix, ...Rest]
|
|
||||||
: never;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typed wrapper for {@link Deno.Kv}
|
|
||||||
*/
|
|
||||||
export class Tkv<K extends Deno.KvKey, T> {
|
|
||||||
constructor(readonly db: Deno.Kv) {}
|
|
||||||
|
|
||||||
get(
|
|
||||||
key: readonly [...K],
|
|
||||||
options: Parameters<Deno.Kv["get"]>[1] = {},
|
|
||||||
): Promise<TkvEntryMaybe<K, T>> {
|
|
||||||
return this.db.get<T>(key, options) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(
|
|
||||||
key: readonly [...K],
|
|
||||||
value: T,
|
|
||||||
options: Parameters<Deno.Kv["set"]>[2] = {},
|
|
||||||
): ReturnType<Deno.Kv["set"]> {
|
|
||||||
return this.db.set(key, value, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
atomicSet(
|
|
||||||
key: readonly [...K],
|
|
||||||
versionstamp: Parameters<Deno.AtomicOperation["check"]>[0]["versionstamp"],
|
|
||||||
value: T,
|
|
||||||
options: Parameters<Deno.AtomicOperation["set"]>[2] = {},
|
|
||||||
): ReturnType<Deno.AtomicOperation["commit"]> {
|
|
||||||
return this.db.atomic()
|
|
||||||
.check({ key, versionstamp })
|
|
||||||
.set(key, value, options)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(key: readonly [...K]): ReturnType<Deno.Kv["delete"]> {
|
|
||||||
return this.db.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
atomicDelete(
|
|
||||||
key: readonly [...K],
|
|
||||||
versionstamp: Parameters<Deno.AtomicOperation["check"]>[0]["versionstamp"],
|
|
||||||
): ReturnType<Deno.AtomicOperation["commit"]> {
|
|
||||||
return this.db.atomic()
|
|
||||||
.check({ key, versionstamp })
|
|
||||||
.delete(key)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
list(
|
|
||||||
selector: TkvListSelector<K>,
|
|
||||||
options: Parameters<Deno.Kv["list"]>[1] = {},
|
|
||||||
): AsyncIterableIterator<TkvEntry<K, T>> {
|
|
||||||
return this.db.list<T>(selector as Deno.KvListSelector, options) as any;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { Chat, User } from "grammy_types";
|
import { Chat, User } from "grammy_types";
|
||||||
|
|
||||||
export function formatUserChat(
|
export function formatUserChat(
|
||||||
ctx: {
|
ctx: { from?: User; chat?: Chat; workerInstanceKey?: string },
|
||||||
from?: User | undefined;
|
|
||||||
chat?: Chat | undefined;
|
|
||||||
workerInstanceKey?: string | undefined;
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const msg: string[] = [];
|
const msg: string[] = [];
|
||||||
if (ctx.from) {
|
if (ctx.from) {
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
export interface KvMemoizeOptions<A extends Deno.KvKey, R> {
|
|
||||||
/**
|
|
||||||
* The time in milliseconds until the cached result expires.
|
|
||||||
*/
|
|
||||||
expireIn?: ((result: R, ...args: A) => number) | number | undefined;
|
|
||||||
/**
|
|
||||||
* Whether to recalculate the result if it was already cached.
|
|
||||||
*
|
|
||||||
* Runs whenever the result is retrieved from the cache.
|
|
||||||
*/
|
|
||||||
shouldRecalculate?: ((result: R, ...args: A) => boolean) | undefined;
|
|
||||||
/**
|
|
||||||
* Whether to cache the result after computing it.
|
|
||||||
*
|
|
||||||
* Runs whenever a new result is computed.
|
|
||||||
*/
|
|
||||||
shouldCache?: ((result: R, ...args: A) => boolean) | undefined;
|
|
||||||
/**
|
|
||||||
* Override the default KV store functions.
|
|
||||||
*/
|
|
||||||
override?: {
|
|
||||||
set: (
|
|
||||||
key: Deno.KvKey,
|
|
||||||
args: A,
|
|
||||||
value: R,
|
|
||||||
options: { expireIn?: number },
|
|
||||||
) => Promise<void>;
|
|
||||||
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Memoizes the function result in KV store.
|
|
||||||
*/
|
|
||||||
export function kvMemoize<A extends Deno.KvKey, R>(
|
|
||||||
db: Deno.Kv,
|
|
||||||
key: Deno.KvKey,
|
|
||||||
fn: (...args: A) => Promise<R>,
|
|
||||||
options?: KvMemoizeOptions<A, R>,
|
|
||||||
): (...args: A) => Promise<R> {
|
|
||||||
return async (...args) => {
|
|
||||||
const cachedResult = options?.override?.get
|
|
||||||
? await options.override.get(key, args)
|
|
||||||
: (await db.get<R>([...key, ...args])).value;
|
|
||||||
|
|
||||||
if (cachedResult != null) {
|
|
||||||
if (!options?.shouldRecalculate?.(cachedResult, ...args)) {
|
|
||||||
return cachedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await fn(...args);
|
|
||||||
|
|
||||||
const expireIn = typeof options?.expireIn === "function"
|
|
||||||
? options.expireIn(result, ...args)
|
|
||||||
: options?.expireIn;
|
|
||||||
|
|
||||||
if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
|
|
||||||
if (options?.override?.set) {
|
|
||||||
await options.override.set(key, args, result, expireIn != null ? { expireIn } : {});
|
|
||||||
} else {
|
|
||||||
await db.set([...key, ...args], result, expireIn != null ? { expireIn } : {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
/**
|
|
||||||
* Removes all undefined properties from an object.
|
|
||||||
*/
|
|
||||||
export function omitUndef<O extends object | undefined>(object: O):
|
|
||||||
& { [K in keyof O as undefined extends O[K] ? never : K]: O[K] }
|
|
||||||
& { [K in keyof O as undefined extends O[K] ? K : never]?: O[K] & ({} | null) } {
|
|
||||||
if (object == undefined) return object as never;
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(object).filter(([, v]) => v !== undefined),
|
|
||||||
) as never;
|
|
||||||
}
|
|
Loading…
Reference in New Issue