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
|
||||||
|
|
27
README.md
27
README.md
|
@ -13,27 +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.
|
|
||||||
- `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:5999/) 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)
|
To connect your SD to the bot, open the [Eris UI](http://localhost:5999/), login as admin and add a
|
||||||
5. Add a new worker in the Eris WebUI.
|
worker.
|
||||||
|
|
||||||
## 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:5999/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:5999/`.
|
|
||||||
- `/util` - Utility functions shared by other parts.
|
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
import { Model } from "indexed_kv";
|
|
||||||
import { info } from "std/log/mod.ts";
|
|
||||||
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
|
||||||
import { Admin, adminSchema, adminStore } from "../app/adminStore.ts";
|
|
||||||
import { getUser, withAdmin, withUser } from "./withUser.ts";
|
|
||||||
|
|
||||||
export type AdminData = Admin & { id: string };
|
|
||||||
|
|
||||||
function getAdminData(adminEntry: Model<Admin>): AdminData {
|
|
||||||
return { id: adminEntry.id, ...adminEntry.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const adminsRoute = createPathFilter({
|
|
||||||
"": createMethodFilter({
|
|
||||||
GET: createEndpoint(
|
|
||||||
{},
|
|
||||||
async () => {
|
|
||||||
const adminEntries = await adminStore.getAll();
|
|
||||||
const admins = adminEntries.map(getAdminData);
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { type: "application/json", data: admins satisfies AdminData[] },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
POST: createEndpoint(
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
sessionId: { type: "string" },
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
type: "application/json",
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
tgUserId: adminSchema.properties.tgUserId,
|
|
||||||
},
|
|
||||||
required: ["tgUserId"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ query, body }) => {
|
|
||||||
return withAdmin(query, async (user, adminEntry) => {
|
|
||||||
const newAdminUser = await getUser(body.data.tgUserId);
|
|
||||||
const newAdminEntry = await adminStore.create({
|
|
||||||
tgUserId: body.data.tgUserId,
|
|
||||||
promotedBy: adminEntry.id,
|
|
||||||
});
|
|
||||||
info(`User ${user.first_name} promoted user ${newAdminUser.first_name} to admin`);
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { type: "application/json", data: getAdminData(newAdminEntry) },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
"promote_self": createMethodFilter({
|
|
||||||
POST: createEndpoint(
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
sessionId: { type: "string" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// if there are no admins, allow any user to promote themselves
|
|
||||||
async ({ query }) => {
|
|
||||||
return withUser(query, async (user) => {
|
|
||||||
const adminEntries = await adminStore.getAll();
|
|
||||||
if (adminEntries.length === 0) {
|
|
||||||
const newAdminEntry = await adminStore.create({
|
|
||||||
tgUserId: user.id,
|
|
||||||
promotedBy: null,
|
|
||||||
});
|
|
||||||
info(`User ${user.first_name} promoted themselves to admin`);
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { type: "application/json", data: getAdminData(newAdminEntry) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 403,
|
|
||||||
body: { type: "text/plain", data: `You are not allowed to promote yourself` },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
"{adminId}": createPathFilter({
|
|
||||||
"": createMethodFilter({
|
|
||||||
GET: createEndpoint(
|
|
||||||
{},
|
|
||||||
async ({ params }) => {
|
|
||||||
const adminEntry = await adminStore.getById(params.adminId!);
|
|
||||||
if (!adminEntry) {
|
|
||||||
return { status: 404, body: { type: "text/plain", data: `Admin not found` } };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { type: "application/json", data: getAdminData(adminEntry) },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DELETE: createEndpoint(
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
sessionId: { type: "string" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ params, query }) => {
|
|
||||||
const deletedAdminEntry = await adminStore.getById(params.adminId!);
|
|
||||||
if (!deletedAdminEntry) {
|
|
||||||
return { status: 404, body: { type: "text/plain", data: `Admin not found` } };
|
|
||||||
}
|
|
||||||
const deletedAdminUser = await getUser(deletedAdminEntry.value.tgUserId);
|
|
||||||
return withUser(query, async (chat) => {
|
|
||||||
await deletedAdminEntry.delete();
|
|
||||||
info(`User ${chat.first_name} demoted user ${deletedAdminUser.first_name} from admin`);
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { type: "application/json", data: null },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { createEndpoint, createMethodFilter } from "t_rest/server";
|
|
||||||
import { bot } from "../bot/mod.ts";
|
|
||||||
|
|
||||||
export const botRoute = createMethodFilter({
|
|
||||||
GET: createEndpoint({ query: null, body: null }, async () => {
|
|
||||||
const username = bot.botInfo.username;
|
|
||||||
return { status: 200, body: { type: "application/json", data: { username } } };
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { route } from "reroute";
|
import { route } from "reroute";
|
||||||
import { serveSpa } from "serve_spa";
|
import { serveSpa } from "serve_spa";
|
||||||
import { serveApi } from "./serveApi.ts";
|
import { serveApi } from "./serveApi.ts";
|
||||||
import { fromFileUrl } from "std/path/mod.ts";
|
import { fromFileUrl } from "std/path/mod.ts"
|
||||||
|
|
||||||
export async function serveUi() {
|
export async function serveUi() {
|
||||||
const server = Deno.serve({ port: 5999 }, (request) =>
|
const server = Deno.serve({ port: 5999 }, (request) =>
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { deepMerge } from "std/collections/deep_merge.ts";
|
||||||
import { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
import { createEndpoint, createMethodFilter } from "t_rest/server";
|
import { createEndpoint, createMethodFilter } from "t_rest/server";
|
||||||
import { configSchema, getConfig, setConfig } from "../app/config.ts";
|
import { configSchema, getConfig, setConfig } from "../app/config.ts";
|
||||||
import { withAdmin } from "./withUser.ts";
|
import { bot } from "../bot/mod.ts";
|
||||||
|
import { sessions } from "./sessionsRoute.ts";
|
||||||
|
|
||||||
export const paramsRoute = createMethodFilter({
|
export const paramsRoute = createMethodFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
|
@ -22,13 +23,23 @@ export const paramsRoute = createMethodFilter({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ query, body }) => {
|
async ({ query, body }) => {
|
||||||
return withAdmin(query, async (user) => {
|
const session = sessions.get(query.sessionId);
|
||||||
const config = await getConfig();
|
if (!session?.userId) {
|
||||||
info(`User ${user.first_name} updated default params: ${JSON.stringify(body.data)}`);
|
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
|
||||||
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
|
}
|
||||||
await setConfig({ defaultParams });
|
const chat = await bot.api.getChat(session.userId);
|
||||||
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
|
if (chat.type !== "private") throw new Error("Chat is not private");
|
||||||
});
|
if (!chat.username) {
|
||||||
|
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
|
||||||
|
}
|
||||||
|
const config = await getConfig();
|
||||||
|
if (!config?.adminUsernames?.includes(chat.username)) {
|
||||||
|
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
|
||||||
|
}
|
||||||
|
info(`User ${chat.username} updated default params: ${JSON.stringify(body.data)}`);
|
||||||
|
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
|
||||||
|
await setConfig({ defaultParams });
|
||||||
|
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import { createLoggerMiddleware, createPathFilter } from "t_rest/server";
|
import {
|
||||||
import { adminsRoute } from "./adminsRoute.ts";
|
createEndpoint,
|
||||||
import { botRoute } from "./botRoute.ts";
|
createLoggerMiddleware,
|
||||||
|
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 serveApi = createLoggerMiddleware(
|
export const serveApi = createLoggerMiddleware(
|
||||||
createPathFilter({
|
createPathFilter({
|
||||||
"admins": adminsRoute,
|
|
||||||
"bot": botRoute,
|
|
||||||
"jobs": jobsRoute,
|
"jobs": jobsRoute,
|
||||||
"sessions": sessionsRoute,
|
"sessions": sessionsRoute,
|
||||||
|
"users": usersRoute,
|
||||||
"settings/params": paramsRoute,
|
"settings/params": paramsRoute,
|
||||||
"stats": statsRoute,
|
"stats": statsRoute,
|
||||||
"users": usersRoute,
|
|
||||||
"workers": workersRoute,
|
"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 } } };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
{ filterStatus: (status) => status >= 400 },
|
{ filterStatus: (status) => status >= 400 },
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
// deno-lint-ignore-file require-await
|
||||||
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
export const sessions = new Map<string, Session>();
|
export const sessions = new Map<string, Session>();
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
userId?: number | undefined;
|
userId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sessionsRoute = createPathFilter({
|
export const sessionsRoute = createPathFilter({
|
||||||
|
@ -24,7 +25,7 @@ export const sessionsRoute = createPathFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ 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) {
|
||||||
return { status: 401, body: { type: "text/plain", data: "Session not found" } };
|
return { status: 401, body: { type: "text/plain", data: "Session not found" } };
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { subMinutes } from "date-fns";
|
|
||||||
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
import { 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 } from "../app/userDailyStatsStore.ts";
|
import { getDailyStats } from "../app/dailyStatsStore.ts";
|
||||||
import { getUserStats } from "../app/userStatsStore.ts";
|
import { getUserStats } from "../app/userStatsStore.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;
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
||||||
|
import { getConfig } from "../app/config.ts";
|
||||||
import { bot } from "../bot/mod.ts";
|
import { bot } from "../bot/mod.ts";
|
||||||
import { getUser } from "./withUser.ts";
|
|
||||||
import { adminStore } from "../app/adminStore.ts";
|
|
||||||
|
|
||||||
export const usersRoute = createPathFilter({
|
export const usersRoute = createPathFilter({
|
||||||
"{userId}/photo": createMethodFilter({
|
"{userId}/photo": createMethodFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ query: null, body: null },
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const user = await getUser(Number(params.userId!));
|
const chat = await bot.api.getChat(params.userId);
|
||||||
const photoData = user.photo?.small_file_id
|
if (chat.type !== "private") {
|
||||||
|
throw new Error("Chat is not private");
|
||||||
|
}
|
||||||
|
const photoData = chat.photo?.small_file_id
|
||||||
? await fetch(
|
? await fetch(
|
||||||
`https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile(
|
`https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile(
|
||||||
user.photo.small_file_id,
|
chat.photo.small_file_id,
|
||||||
).then((file) => file.file_path)}`,
|
).then((file) => file.file_path)}`,
|
||||||
).then((resp) => resp.arrayBuffer())
|
).then((resp) => resp.arrayBuffer())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -34,14 +36,17 @@ export const usersRoute = createPathFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ query: null, body: null },
|
||||||
async ({ params }) => {
|
async ({ params }) => {
|
||||||
const user = await getUser(Number(params.userId!));
|
const chat = await bot.api.getChat(params.userId);
|
||||||
const [adminEntry] = await adminStore.getBy("tgUserId", { value: user.id });
|
if (chat.type !== "private") {
|
||||||
const admin = adminEntry?.value;
|
throw new Error("Chat is not private");
|
||||||
|
}
|
||||||
|
const config = await getConfig();
|
||||||
|
const isAdmin = chat.username && config?.adminUsernames?.includes(chat.username);
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
data: { ...user, admin },
|
data: { ...chat, isAdmin },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { Chat } from "grammy_types";
|
|
||||||
import { Model } from "indexed_kv";
|
|
||||||
import { Output } from "t_rest/client";
|
|
||||||
import { Admin, adminStore } from "../app/adminStore.ts";
|
|
||||||
import { bot } from "../bot/mod.ts";
|
|
||||||
import { sessions } from "./sessionsRoute.ts";
|
|
||||||
|
|
||||||
export async function withUser<O extends Output>(
|
|
||||||
query: { sessionId: string },
|
|
||||||
cb: (user: Chat.PrivateGetChat) => Promise<O>,
|
|
||||||
) {
|
|
||||||
const session = sessions.get(query.sessionId);
|
|
||||||
if (!session?.userId) {
|
|
||||||
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } } as const;
|
|
||||||
}
|
|
||||||
const user = await getUser(session.userId);
|
|
||||||
return cb(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function withAdmin<O extends Output>(
|
|
||||||
query: { sessionId: string },
|
|
||||||
cb: (user: Chat.PrivateGetChat, admin: Model<Admin>) => Promise<O>,
|
|
||||||
) {
|
|
||||||
const session = sessions.get(query.sessionId);
|
|
||||||
if (!session?.userId) {
|
|
||||||
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } } as const;
|
|
||||||
}
|
|
||||||
const user = await getUser(session.userId);
|
|
||||||
const [admin] = await adminStore.getBy("tgUserId", { value: session.userId });
|
|
||||||
if (!admin) {
|
|
||||||
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } } as const;
|
|
||||||
}
|
|
||||||
return cb(user, admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -3,17 +3,14 @@ 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 { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
|
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 { withAdmin } from "./withUser.ts";
|
|
||||||
|
|
||||||
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
|
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -72,6 +69,7 @@ export const workersRoute = createPathFilter({
|
||||||
async () => {
|
async () => {
|
||||||
const workerInstances = await workerInstanceStore.getAll();
|
const workerInstances = await workerInstanceStore.getAll();
|
||||||
const workers = await Promise.all(workerInstances.map(getWorkerData));
|
const workers = await Promise.all(workerInstances.map(getWorkerData));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: { type: "application/json", data: workers satisfies WorkerData[] },
|
body: { type: "application/json", data: workers satisfies WorkerData[] },
|
||||||
|
@ -85,18 +83,51 @@ export const workersRoute = createPathFilter({
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
schema: workerInstanceSchema,
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
key: { type: "string" },
|
||||||
|
name: { type: ["string", "null"] },
|
||||||
|
sdUrl: { type: "string" },
|
||||||
|
sdAuth: {
|
||||||
|
type: ["object", "null"],
|
||||||
|
properties: {
|
||||||
|
user: { type: "string" },
|
||||||
|
password: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["user", "password"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["key", "name", "sdUrl", "sdAuth"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ query, body }) => {
|
async ({ query, body }) => {
|
||||||
return withAdmin(query, async (user) => {
|
const session = sessions.get(query.sessionId);
|
||||||
const workerInstance = await workerInstanceStore.create(body.data);
|
if (!session?.userId) {
|
||||||
info(`User ${user.first_name} created worker ${workerInstance.value.name}`);
|
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
|
||||||
return {
|
}
|
||||||
status: 200,
|
const chat = await bot.api.getChat(session.userId);
|
||||||
body: { type: "application/json", data: await getWorkerData(workerInstance) },
|
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 },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -106,13 +137,14 @@ export const workersRoute = createPathFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ 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) {
|
||||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
|
const worker: WorkerData = await getWorkerData(workerInstance);
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: { type: "application/json", data: await getWorkerData(workerInstance) },
|
body: { type: "application/json", data: worker satisfies WorkerData },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -123,26 +155,60 @@ export const workersRoute = createPathFilter({
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
schema: { ...workerInstanceSchema, required: [] },
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
key: { type: "string" },
|
||||||
|
name: { type: ["string", "null"] },
|
||||||
|
sdUrl: { type: "string" },
|
||||||
|
auth: {
|
||||||
|
type: ["object", "null"],
|
||||||
|
properties: {
|
||||||
|
user: { type: "string" },
|
||||||
|
password: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["user", "password"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ params, query, body }) => {
|
async ({ params, query, body }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
return withAdmin(query, async (user) => {
|
const session = sessions.get(query.sessionId);
|
||||||
info(
|
if (!session?.userId) {
|
||||||
`User ${user.first_name} updated worker ${workerInstance.value.name}: ${
|
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
|
||||||
JSON.stringify(body.data)
|
}
|
||||||
}`,
|
const chat = await bot.api.getChat(session.userId);
|
||||||
);
|
if (chat.type !== "private") throw new Error("Chat is not private");
|
||||||
await workerInstance.update(omitUndef(body.data));
|
if (!chat.username) {
|
||||||
return {
|
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
|
||||||
status: 200,
|
}
|
||||||
body: { type: "application/json", data: await getWorkerData(workerInstance) },
|
const config = await getConfig();
|
||||||
};
|
if (!config?.adminUsernames?.includes(chat.username)) {
|
||||||
});
|
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
|
||||||
|
}
|
||||||
|
if (body.data.name !== undefined) {
|
||||||
|
workerInstance.value.name = body.data.name;
|
||||||
|
}
|
||||||
|
if (body.data.sdUrl !== undefined) {
|
||||||
|
workerInstance.value.sdUrl = body.data.sdUrl;
|
||||||
|
}
|
||||||
|
if (body.data.auth !== undefined) {
|
||||||
|
workerInstance.value.sdAuth = body.data.auth;
|
||||||
|
}
|
||||||
|
info(
|
||||||
|
`User ${chat.username} updated worker ${params.workerId}: ${JSON.stringify(body.data)}`,
|
||||||
|
);
|
||||||
|
await workerInstance.update();
|
||||||
|
const worker = await getWorkerData(workerInstance);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { type: "application/json", data: worker satisfies WorkerData },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
DELETE: createEndpoint(
|
DELETE: createEndpoint(
|
||||||
|
@ -153,15 +219,26 @@ export const workersRoute = createPathFilter({
|
||||||
body: null,
|
body: null,
|
||||||
},
|
},
|
||||||
async ({ params, query }) => {
|
async ({ params, query }) => {
|
||||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||||
if (!workerInstance) {
|
if (!workerInstance) {
|
||||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
return withAdmin(query, async (user) => {
|
const session = sessions.get(query.sessionId);
|
||||||
info(`User ${user.first_name} deleted worker ${workerInstance.value.name}`);
|
if (!session?.userId) {
|
||||||
await workerInstance.delete();
|
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } };
|
||||||
return { status: 200, body: { type: "application/json", data: null } };
|
}
|
||||||
});
|
const chat = await bot.api.getChat(session.userId);
|
||||||
|
if (chat.type !== "private") throw new Error("Chat is not private");
|
||||||
|
if (!chat.username) {
|
||||||
|
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
|
||||||
|
}
|
||||||
|
const config = await getConfig();
|
||||||
|
if (!config?.adminUsernames?.includes(chat.username)) {
|
||||||
|
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
|
||||||
|
}
|
||||||
|
info(`User ${chat.username} deleted worker ${params.workerId}`);
|
||||||
|
await workerInstance.delete();
|
||||||
|
return { status: 200, body: { type: "application/json", data: null } };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -170,7 +247,7 @@ export const workersRoute = createPathFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ 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) {
|
||||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
|
@ -201,7 +278,7 @@ export const workersRoute = createPathFilter({
|
||||||
GET: createEndpoint(
|
GET: createEndpoint(
|
||||||
{ query: null, body: null },
|
{ 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) {
|
||||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Store } from "indexed_kv";
|
|
||||||
import { JsonSchema, jsonType } from "t_rest/server";
|
|
||||||
import { db } from "./db.ts";
|
|
||||||
|
|
||||||
export const adminSchema = {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
tgUserId: { type: "number" },
|
|
||||||
promotedBy: { type: ["string", "null"] },
|
|
||||||
},
|
|
||||||
required: ["tgUserId", "promotedBy"],
|
|
||||||
} as const satisfies JsonSchema;
|
|
||||||
|
|
||||||
export type Admin = jsonType<typeof adminSchema>;
|
|
||||||
|
|
||||||
type AdminIndices = {
|
|
||||||
tgUserId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const adminStore = new Store<Admin, AdminIndices>(db, "adminUsers", {
|
|
||||||
indices: {
|
|
||||||
tgUserId: { getValue: (adminUser) => adminUser.tgUserId },
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -4,6 +4,7 @@ import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
export const configSchema = {
|
export const configSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
adminUsernames: { type: "array", items: { type: "string" } },
|
||||||
pausedReason: { type: ["string", "null"] },
|
pausedReason: { type: ["string", "null"] },
|
||||||
maxUserJobs: { type: "number" },
|
maxUserJobs: { type: "number" },
|
||||||
maxJobs: { type: "number" },
|
maxJobs: { type: "number" },
|
||||||
|
@ -30,6 +31,7 @@ export async function getConfig(): Promise<Config> {
|
||||||
const configEntry = await db.get<Config>(["config"]);
|
const configEntry = await db.get<Config>(["config"]);
|
||||||
const config = configEntry?.value;
|
const config = configEntry?.value;
|
||||||
return {
|
return {
|
||||||
|
adminUsernames: config?.adminUsernames ?? Deno.env.get("TG_ADMIN_USERNAMES")?.split(",") ?? [],
|
||||||
pausedReason: config?.pausedReason ?? null,
|
pausedReason: config?.pausedReason ?? null,
|
||||||
maxUserJobs: config?.maxUserJobs ?? Infinity,
|
maxUserJobs: config?.maxUserJobs ?? Infinity,
|
||||||
maxJobs: config?.maxJobs ?? Infinity,
|
maxJobs: config?.maxJobs ?? Infinity,
|
||||||
|
@ -40,6 +42,7 @@ export async function getConfig(): Promise<Config> {
|
||||||
export async function setConfig(newConfig: Partial<Config>): Promise<void> {
|
export async function setConfig(newConfig: Partial<Config>): Promise<void> {
|
||||||
const oldConfig = await getConfig();
|
const oldConfig = await getConfig();
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
|
adminUsernames: newConfig.adminUsernames ?? oldConfig.adminUsernames,
|
||||||
pausedReason: newConfig.pausedReason ?? oldConfig.pausedReason,
|
pausedReason: newConfig.pausedReason ?? oldConfig.pausedReason,
|
||||||
maxUserJobs: newConfig.maxUserJobs ?? oldConfig.maxUserJobs,
|
maxUserJobs: newConfig.maxUserJobs ?? oldConfig.maxUserJobs,
|
||||||
maxJobs: newConfig.maxJobs ?? oldConfig.maxJobs,
|
maxJobs: newConfig.maxJobs ?? oldConfig.maxJobs,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
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 { info } from "std/log/mod.ts";
|
import { info } from "std/log/mod.ts";
|
||||||
import { JsonSchema, jsonType } from "t_rest/server";
|
import { JsonSchema, jsonType } from "t_rest/server";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -11,7 +11,6 @@ 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";
|
||||||
|
@ -162,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(
|
||||||
|
@ -183,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,
|
||||||
|
@ -210,7 +209,7 @@ async function processGenerationJob(
|
||||||
).then((resp) => resp.arrayBuffer()),
|
).then((resp) => resp.arrayBuffer()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -306,24 +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;
|
||||||
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;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
await bot.api.editMessageText(
|
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 },
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
}));
|
}
|
||||||
|
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,42 +1,19 @@
|
||||||
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.
|
* Memoizes the function result in KV storage.
|
||||||
*/
|
*/
|
||||||
export function kvMemoize<A extends Deno.KvKey, R>(
|
export function kvMemoize<A extends Deno.KvKey, R>(
|
||||||
db: Deno.Kv,
|
db: Deno.Kv,
|
||||||
key: Deno.KvKey,
|
key: Deno.KvKey,
|
||||||
fn: (...args: A) => Promise<R>,
|
fn: (...args: A) => Promise<R>,
|
||||||
options?: KvMemoizeOptions<A, 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> {
|
): (...args: A) => Promise<R> {
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
const cachedResult = options?.override?.get
|
const cachedResult = options?.override?.get
|
||||||
|
@ -57,9 +34,9 @@ export function kvMemoize<A extends Deno.KvKey, R>(
|
||||||
|
|
||||||
if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
|
if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
|
||||||
if (options?.override?.set) {
|
if (options?.override?.set) {
|
||||||
await options.override.set(key, args, result, expireIn != null ? { expireIn } : {});
|
await options.override.set(key, args, result, { expireIn });
|
||||||
} else {
|
} else {
|
||||||
await db.set([...key, ...args], result, expireIn != null ? { expireIn } : {});
|
await db.set([...key, ...args], result, { expireIn });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`);
|
||||||
|
|
|
@ -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 [admin] = await adminStore.getBy("tgUserId", { value: ctx.from.id });
|
const config = await getConfig();
|
||||||
|
|
||||||
if (!admin) {
|
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`,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
omitUndef({
|
allow_sending_without_reply: true,
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
});
|
||||||
allow_sending_without_reply: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
68
bot/mod.ts
68
bot/mod.ts
|
@ -4,8 +4,8 @@ import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
|
||||||
import { run, 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";
|
||||||
|
@ -19,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 =
|
||||||
|
@ -94,13 +94,10 @@ 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}`,
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
omitUndef({
|
allow_sending_without_reply: true,
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
});
|
||||||
allow_sending_without_reply: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -125,33 +122,24 @@ bot.command("start", async (ctx) => {
|
||||||
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",
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
omitUndef({
|
});
|
||||||
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.",
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
omitUndef({
|
});
|
||||||
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",
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
omitUndef({
|
});
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("txt2img", txt2imgCommand);
|
bot.command("txt2img", txt2imgCommand);
|
||||||
|
@ -169,6 +157,30 @@ bot.command("cancel", cancelCommand);
|
||||||
|
|
||||||
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");
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import extractChunks from "png_chunks_extract";
|
||||||
|
|
||||||
export function getPngInfo(pngData: Uint8Array): string | undefined {
|
export function getPngInfo(pngData: Uint8Array): string | undefined {
|
||||||
return extractChunks(pngData)
|
return extractChunks(pngData)
|
||||||
.filter((chunk) => chunk.name === "tEXt")
|
.filter((chunk) => chunk.name === "tEXt" || chunk.name === "iTXt")
|
||||||
.map((chunk) => decode(chunk.data))
|
.map((chunk) => decode(chunk.data))
|
||||||
.find((textChunk) => textChunk.keyword === "parameters")
|
.find((textChunk) => textChunk.keyword === "parameters")
|
||||||
?.text;
|
?.text;
|
||||||
|
@ -38,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":
|
||||||
|
@ -71,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);
|
||||||
|
@ -107,9 +107,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
@ -24,13 +23,11 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Please send me a PNG 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;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +36,13 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
||||||
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
const buffer = await fetch(file.getUrl()).then((resp) => resp.arrayBuffer());
|
||||||
const params = parsePngInfo(getPngInfo(new Uint8Array(buffer)) ?? "");
|
const params = parsePngInfo(getPngInfo(new Uint8Array(buffer)) ?? "");
|
||||||
|
|
||||||
|
if (!params.prompt) {
|
||||||
|
ctx.reply("File doesn't contain any parameters.", {
|
||||||
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const paramsText = fmt([
|
const paramsText = fmt([
|
||||||
`${params.prompt}\n`,
|
`${params.prompt}\n`,
|
||||||
params.negative_prompt ? fmt`${bold("Negative prompt:")} ${params.negative_prompt}\n` : "",
|
params.negative_prompt ? fmt`${bold("Negative prompt:")} ${params.negative_prompt}\n` : "",
|
||||||
|
@ -49,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,
|
entities: paramsText.entities,
|
||||||
omitUndef({
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
entities: paramsText.entities,
|
});
|
||||||
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,
|
disable_notification: true,
|
||||||
omitUndef({
|
reply_to_message_id: ctx.message?.message_id,
|
||||||
disable_notification: true,
|
});
|
||||||
reply_to_message_id: ctx.message?.message_id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
handleFutureUpdates().catch(() => undefined);
|
handleFutureUpdates().catch(() => undefined);
|
||||||
|
|
||||||
async function getMessageText() {
|
async function getMessageText() {
|
||||||
|
|
53
deno.json
53
deno.json
|
@ -1,35 +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",
|
|
||||||
"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",
|
||||||
"png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0",
|
"png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0",
|
||||||
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
|
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
|
||||||
"react": "https://esm.sh/react@18.2.0?dev",
|
|
||||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?external=react&dev",
|
|
||||||
"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/",
|
||||||
|
@ -39,25 +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",
|
||||||
"t_rest/client": "https://esm.sh/ty-rest@0.4.1/client",
|
|
||||||
"t_rest/server": "https://esm.sh/ty-rest@0.4.1/server",
|
"@twind/core": "https://esm.sh/@twind/core@1.1.3?dev",
|
||||||
"twind/core": "https://esm.sh/@twind/core@1.1.3",
|
"@twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4?dev",
|
||||||
"twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4",
|
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
||||||
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts"
|
"react-flip-move": "https://esm.sh/react-flip-move@3.0.5?dev",
|
||||||
},
|
"react-intl": "https://esm.sh/react-intl@6.4.7?external=react&alias=@types/react:react&dev",
|
||||||
"lint": {
|
"react-router-dom": "https://esm.sh/react-router-dom@6.16.0?dev",
|
||||||
"rules": {
|
"react": "https://esm.sh/react@18.2.0?dev",
|
||||||
"exclude": [
|
"swr": "https://esm.sh/swr@2.2.4?dev",
|
||||||
"require-await"
|
"swr/mutation": "https://esm.sh/swr@2.2.4/mutation?dev",
|
||||||
]
|
"t_rest/client": "https://esm.sh/ty-rest@0.4.0/client?dev",
|
||||||
}
|
"use-local-storage": "https://esm.sh/use-local-storage@3.0.0?dev"
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
152
deno.lock
152
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": {
|
||||||
|
@ -235,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",
|
||||||
|
@ -273,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",
|
||||||
|
@ -288,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",
|
||||||
|
@ -300,69 +300,63 @@
|
||||||
"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/@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/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/png-chunk-text@1.0.0": "08beb86f31b5ff70240650fe095b6c4e4037e5c1d5917f9338e7633cae680356",
|
"https://esm.sh/png-chunk-text@1.0.0": "08beb86f31b5ff70240650fe095b6c4e4037e5c1d5917f9338e7633cae680356",
|
||||||
"https://esm.sh/png-chunks-extract@1.0.0": "da06bbd3c08199d72ab16354abe5ffd2361cb891ccbb44d757a0a0a4fbfa12b5",
|
"https://esm.sh/png-chunks-extract@1.0.0": "da06bbd3c08199d72ab16354abe5ffd2361cb891ccbb44d757a0a0a4fbfa12b5",
|
||||||
"https://esm.sh/react-dom@18.2.0/client?external=react&dev": "2eb39c339720d727591fd55fb44ffcb6f14b06812af0a71e7a2268185b5b6e73",
|
"https://esm.sh/react-dom@18.2.0/client?dev": "db94655481962a38b32ffbbb0ad50df0a5582ee3e139acb375f989f74b630d82",
|
||||||
"https://esm.sh/react-flip-move@3.0.5?external=react&dev": "4390c0777a0bec583d3e6cb5e4b33831ac937d670d894a20e4f192ce8cd21bae",
|
"https://esm.sh/react-flip-move@3.0.5?dev": "f92489b69efcaba985c9f45b4f4888a24aee7076cf5c03dad378e103d0c1e492",
|
||||||
"https://esm.sh/react-intl@6.4.7?external=react&dev": "60e68890e2c5ef3c02d37a89c53e056b1bbd1c8467a7aae62f0b634abc7a8a5f",
|
"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?external=react&dev": "16046eba15c1ae1ce912e5ab6fdd1f6ce24ea8ac3970d5bdcdb1ebb0e40458c2",
|
"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/@formatjs/ecma402-abstract@1.17.2/X-ZS9yZWFjdA/denonext/ecma402-abstract.development.mjs": "178a303e29d73369b4c7c1da5a18393c2baa8742a0e8a45be85f506c17f763d9",
|
"https://esm.sh/v133/@formatjs/intl@2.9.3/X-YS9AdHlwZXMvcmVhY3Q6cmVhY3QKZS9yZWFjdA/denonext/intl.development.mjs": "b01ab881505e8d29bf9b337f9b48841893203e063d31775eed6ba292acfac298",
|
||||||
"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/client-only@0.0.1/X-ZS9yZWFjdA/denonext/client-only.development.mjs": "b7efaef2653f7f628d084e24a4d5a857c9cd1a5e5060d1d9957e185ee98c8a28",
|
"https://esm.sh/v133/@twind/core@1.1.3/denonext/core.development.mjs": "1dd38a506c4b728456796e87caa55a31c3855edc6a052a391264c96ee6b89f67",
|
||||||
|
"https://esm.sh/v133/@twind/preset-tailwind@1.1.4/denonext/preset-tailwind.development.mjs": "fc8acfed444337d323d8d6ab5d6198cbc4573a26816387c66ebf6524d20bd772",
|
||||||
|
"https://esm.sh/v133/client-only@0.0.1/denonext/client-only.development.mjs": "b7efaef2653f7f628d084e24a4d5a857c9cd1a5e5060d1d9957e185ee98c8a28",
|
||||||
"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/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/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/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/v133/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "f69f67cf655c38428b0934e0f7c865c055834a87cc3866b629d6b2beb21005e9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,250 +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}", "GET", { params: { sessionId } }] as const : null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
const getUser = useSWR(
|
|
||||||
getSession.data?.userId
|
|
||||||
? ["users/{userId}", "GET", { params: { userId: String(getSession.data.userId) } }] as const
|
|
||||||
: null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAdmins = useSWR(
|
|
||||||
["admins", "GET", {}] as const,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{getUser.data && getAdmins.data && getAdmins.data.length === 0 && (
|
|
||||||
<button
|
|
||||||
className="button-filled"
|
|
||||||
onClick={() => {
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "admins",
|
|
||||||
async () =>
|
|
||||||
fetchApi("admins/promote_self", "POST", {
|
|
||||||
query: { sessionId: sessionId ?? "" },
|
|
||||||
}).then(handleResponse),
|
|
||||||
{ populateCache: false },
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Promote me to admin
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{getAdmins.data?.length
|
|
||||||
? (
|
|
||||||
<ul className="my-4 flex flex-col gap-2">
|
|
||||||
{getAdmins.data.map((admin) => (
|
|
||||||
<AdminListItem key={admin.id} admin={admin} sessionId={sessionId} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
: getAdmins.data?.length === 0
|
|
||||||
? <p>No admins</p>
|
|
||||||
: 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);
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "admins",
|
|
||||||
async () =>
|
|
||||||
fetchApi("admins", "POST", {
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
body: {
|
|
||||||
type: "application/json",
|
|
||||||
data: {
|
|
||||||
tgUserId: Number(data.get("tgUserId") as string),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).then(handleResponse),
|
|
||||||
{ populateCache: false },
|
|
||||||
);
|
|
||||||
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}", "GET", { params: { userId: String(admin.tgUserId) } }] as const,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSession = useSWR(
|
|
||||||
sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null,
|
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
|
||||||
);
|
|
||||||
const getUser = useSWR(
|
|
||||||
getSession.data?.userId
|
|
||||||
? ["users/{userId}", "GET", { 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.id} {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.id}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteAdminDialog(props: {
|
|
||||||
dialogRef: React.RefObject<HTMLDialogElement>;
|
|
||||||
adminId: string;
|
|
||||||
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();
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "admins",
|
|
||||||
async () =>
|
|
||||||
fetchApi("admins/{adminId}", "DELETE", {
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
params: { adminId: adminId },
|
|
||||||
}).then(handleResponse),
|
|
||||||
{ populateCache: false },
|
|
||||||
);
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
10
ui/App.tsx
10
ui/App.tsx
|
@ -1,17 +1,16 @@
|
||||||
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";
|
|
||||||
|
|
||||||
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(() => {
|
||||||
|
@ -28,12 +27,11 @@ 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} />} />
|
||||||
|
|
|
@ -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 { fetchApi, handleResponse } from "./apiClient.ts";
|
|
||||||
|
|
||||||
function NavTab(props: { to: string; children: ReactNode }) {
|
function NavTab(props: { to: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -15,35 +15,32 @@ 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}", "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}", "GET", { 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 getBot = useSWR(
|
const bot = useSWR(
|
||||||
["bot", "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", "GET", {
|
? ["users/{userId}/photo", "GET", {
|
||||||
params: { userId: String(getSession.data.userId) },
|
params: { userId: String(session.data.userId) },
|
||||||
}] as const
|
}] as const
|
||||||
: null,
|
: null,
|
||||||
(args) => fetchApi(...args).then(handleResponse).then((blob) => URL.createObjectURL(blob)),
|
(args) => fetchApi(...args).then(handleResponse).then((blob) => URL.createObjectURL(blob)),
|
||||||
|
@ -64,9 +61,6 @@ export function AppHeader(props: {
|
||||||
<NavTab to="/">
|
<NavTab to="/">
|
||||||
Stats
|
Stats
|
||||||
</NavTab>
|
</NavTab>
|
||||||
<NavTab to="/admins">
|
|
||||||
Admins
|
|
||||||
</NavTab>
|
|
||||||
<NavTab to="/workers">
|
<NavTab to="/workers">
|
||||||
Workers
|
Workers
|
||||||
</NavTab>
|
</NavTab>
|
||||||
|
@ -79,29 +73,29 @@ export function AppHeader(props: {
|
||||||
</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
|
||||||
|
@ -110,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,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,10 +3,10 @@ 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", "GET", {}] as const,
|
["jobs", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 2000 },
|
{ refreshInterval: 2000 },
|
||||||
|
@ -19,7 +19,7 @@ export function QueuePage() {
|
||||||
enterAnimation="fade"
|
enterAnimation="fade"
|
||||||
leaveAnimation="fade"
|
leaveAnimation="fade"
|
||||||
>
|
>
|
||||||
{getJobs.data?.map((job) => (
|
{jobs.data?.map((job) => (
|
||||||
<li
|
<li
|
||||||
className="flex items-baseline gap-2 bg-zinc-100 dark:bg-zinc-700 px-2 py-1 rounded-xl"
|
className="flex items-baseline gap-2 bg-zinc-100 dark:bg-zinc-700 px-2 py-1 rounded-xl"
|
||||||
key={job.id.join("/")}
|
key={job.id.join("/")}
|
||||||
|
|
|
@ -1,82 +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";
|
|
||||||
|
|
||||||
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}", "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}", "GET", { 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", "GET", {}] 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", "PATCH", {
|
fetchApi("settings/params", "PATCH", {
|
||||||
query: { sessionId: sessionId ?? "" },
|
query: { sessionId },
|
||||||
body: { type: "application/json", data: newParams ?? {} },
|
body: { type: "application/json", data: changedParams ?? {} },
|
||||||
}).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
|
||||||
|
@ -85,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),
|
||||||
}))}
|
}))}
|
||||||
|
@ -101,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
|
||||||
|
@ -125,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),
|
||||||
}))}
|
}))}
|
||||||
|
@ -141,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"
|
||||||
|
@ -165,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),
|
||||||
}))}
|
}))}
|
||||||
|
@ -181,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),
|
||||||
}))}
|
}))}
|
||||||
|
@ -194,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"
|
||||||
|
@ -202,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),
|
||||||
}))}
|
}))}
|
||||||
|
@ -218,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>
|
||||||
|
@ -273,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,10 +1,10 @@
|
||||||
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", "GET", {}] as const,
|
["stats", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 2_000 },
|
{ refreshInterval: 2_000 },
|
||||||
|
@ -16,13 +16,13 @@ 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={3_000}
|
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={2_000}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
|
@ -32,13 +32,13 @@ 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={3_000}
|
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={2_000}
|
transitionDurationMs={2_000}
|
||||||
postfix="/s"
|
postfix="/s"
|
||||||
|
@ -49,13 +49,13 @@ 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={3_000}
|
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={2_000}
|
transitionDurationMs={2_000}
|
||||||
|
@ -66,13 +66,13 @@ 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={3_000}
|
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={2_000}
|
transitionDurationMs={2_000}
|
||||||
|
@ -84,7 +84,7 @@ 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_500}
|
transitionDurationMs={1_500}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import React, { RefObject, useRef, useState } 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 { WorkerData } 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}", "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),
|
||||||
);
|
);
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["users/{userId}", "GET", { 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", "GET", {}] as const,
|
["workers", "GET", {}] as const,
|
||||||
(args) => fetchApi(...args).then(handleResponse),
|
(args) => fetchApi(...args).then(handleResponse),
|
||||||
{ refreshInterval: 5000 },
|
{ refreshInterval: 5000 },
|
||||||
|
@ -29,63 +29,37 @@ export function WorkersPage(props: { sessionId: string | null }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getWorkers.data?.length
|
<ul className="my-4 flex flex-col gap-2">
|
||||||
? (
|
{workers.data?.map((worker) => (
|
||||||
<ul className="my-4 flex flex-col gap-2">
|
<WorkerListItem key={worker.id} worker={worker} sessionId={sessionId} />
|
||||||
{getWorkers.data?.map((worker) => (
|
))}
|
||||||
<WorkerListItem key={worker.id} worker={worker} sessionId={sessionId} />
|
</ul>
|
||||||
))}
|
{user.data?.isAdmin && (
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
: getWorkers.data?.length === 0
|
|
||||||
? <p>No workers</p>
|
|
||||||
: 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}
|
>
|
||||||
/>
|
<form
|
||||||
</>
|
method="dialog"
|
||||||
);
|
className="flex flex-col gap-4 p-4"
|
||||||
}
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
function AddWorkerDialog(props: {
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
const key = data.get("key") as string;
|
||||||
sessionId: string | null;
|
const name = data.get("name") as string;
|
||||||
}) {
|
const sdUrl = data.get("url") as string;
|
||||||
const { dialogRef, sessionId } = props;
|
const user = data.get("user") as string;
|
||||||
|
const password = data.get("password") as string;
|
||||||
const { mutate } = useSWRConfig();
|
console.log(key, name, user, password);
|
||||||
|
workers.mutate(async () => {
|
||||||
return (
|
const worker = await fetchApi("workers", "POST", {
|
||||||
<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);
|
|
||||||
const key = data.get("key") as string;
|
|
||||||
const name = data.get("name") as string;
|
|
||||||
const sdUrl = data.get("url") as string;
|
|
||||||
const user = data.get("user") as string;
|
|
||||||
const password = data.get("password") as string;
|
|
||||||
console.log(key, name, user, password);
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "workers",
|
|
||||||
async () =>
|
|
||||||
fetchApi("workers", "POST", {
|
|
||||||
query: { sessionId: sessionId! },
|
query: { sessionId: sessionId! },
|
||||||
body: {
|
body: {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
|
@ -96,112 +70,113 @@ function AddWorkerDialog(props: {
|
||||||
sdAuth: user && password ? { user, password } : null,
|
sdAuth: user && password ? { user, password } : null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).then(handleResponse),
|
}).then(handleResponse);
|
||||||
{ populateCache: false },
|
return [...(workers.data ?? []), worker];
|
||||||
);
|
});
|
||||||
dialogRef.current?.close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
Key
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="text"
|
|
||||||
name="key"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-zinc-500">
|
|
||||||
Used for counting statistics
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
Name
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-zinc-500">
|
|
||||||
Used for display
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label className="flex flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
URL
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="url"
|
|
||||||
name="url"
|
|
||||||
required
|
|
||||||
pattern="https?://.*"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
User
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="text"
|
|
||||||
name="user"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
Password
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center justify-end">
|
createWorkerModalRef.current?.close();
|
||||||
<button
|
}}
|
||||||
type="button"
|
>
|
||||||
className="button-outlined"
|
<div className="flex gap-4">
|
||||||
onClick={() => dialogRef.current?.close()}
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
>
|
<span className="text-sm">
|
||||||
Close
|
Key
|
||||||
</button>
|
</span>
|
||||||
<button type="submit" disabled={!sessionId} className="button-filled">
|
<input
|
||||||
Save
|
className="input-text"
|
||||||
</button>
|
type="text"
|
||||||
</div>
|
name="key"
|
||||||
</form>
|
required
|
||||||
</dialog>
|
/>
|
||||||
|
<span className="text-sm text-zinc-500">
|
||||||
|
Used for counting statistics
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
Name
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-zinc-500">
|
||||||
|
Used for display
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="flex flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
URL
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="url"
|
||||||
|
name="url"
|
||||||
|
required
|
||||||
|
pattern="https?://.*"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
User
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
Password
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-center justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-outlined"
|
||||||
|
onClick={() => createWorkerModalRef.current?.close()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" disabled={!sessionId} className="button-filled">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkerListItem(props: {
|
function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
|
||||||
worker: WorkerData;
|
|
||||||
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}", "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),
|
||||||
);
|
);
|
||||||
const getUser = useSWR(
|
const user = useSWR(
|
||||||
getSession.data?.userId
|
session.data?.userId
|
||||||
? ["users/{userId}", "GET", { 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">
|
||||||
|
@ -237,162 +212,119 @@ 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
|
<>
|
||||||
className="button-outlined"
|
<button
|
||||||
onClick={() => editDialogRef.current?.showModal()}
|
className="button-outlined"
|
||||||
>
|
onClick={() => editWorkerModalRef.current?.showModal()}
|
||||||
Settings
|
>
|
||||||
</button>
|
Settings
|
||||||
<button
|
</button>
|
||||||
className="button-outlined"
|
<button
|
||||||
onClick={() => deleteDialogRef.current?.showModal()}
|
className="button-outlined"
|
||||||
>
|
onClick={() => deleteWorkerModalRef.current?.showModal()}
|
||||||
Delete
|
>
|
||||||
</button>
|
Delete
|
||||||
</p>
|
</button>
|
||||||
)}
|
</>
|
||||||
<EditWorkerDialog
|
)}
|
||||||
dialogRef={editDialogRef}
|
</p>
|
||||||
workerId={worker.id}
|
<dialog
|
||||||
sessionId={sessionId}
|
className="dialog"
|
||||||
/>
|
ref={editWorkerModalRef}
|
||||||
<DeleteWorkerDialog
|
>
|
||||||
dialogRef={deleteDialogRef}
|
<form
|
||||||
workerId={worker.id}
|
method="dialog"
|
||||||
sessionId={sessionId}
|
className="flex flex-col gap-4 p-4"
|
||||||
/>
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
|
const user = data.get("user") as string;
|
||||||
|
const password = data.get("password") as string;
|
||||||
|
console.log(user, password);
|
||||||
|
fetchApi("workers/{workerId}", "PATCH", {
|
||||||
|
params: { workerId: worker.id },
|
||||||
|
query: { sessionId: sessionId! },
|
||||||
|
body: {
|
||||||
|
type: "application/json",
|
||||||
|
data: {
|
||||||
|
auth: user && password ? { user, password } : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
editWorkerModalRef.current?.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
User
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-1 flex-col items-stretch gap-1">
|
||||||
|
<span className="text-sm">
|
||||||
|
Password
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="input-text"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-outlined"
|
||||||
|
onClick={() => editWorkerModalRef.current?.close()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" disabled={!sessionId} className="button-filled">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<dialog
|
||||||
|
className="dialog"
|
||||||
|
ref={deleteWorkerModalRef}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
method="dialog"
|
||||||
|
className="flex flex-col gap-4 p-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchApi("workers/{workerId}", "DELETE", {
|
||||||
|
params: { workerId: worker.id },
|
||||||
|
query: { sessionId: sessionId! },
|
||||||
|
}).then(handleResponse).then(() => mutate(["workers", "GET", {}]));
|
||||||
|
deleteWorkerModalRef.current?.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete this worker?
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 items-center justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-outlined"
|
||||||
|
onClick={() => deleteWorkerModalRef.current?.close()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" disabled={!sessionId} className="button-filled">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditWorkerDialog(props: {
|
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
|
||||||
workerId: string;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, workerId, sessionId } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog
|
|
||||||
className="dialog animate-pop-in backdrop-animate-fade-in"
|
|
||||||
ref={dialogRef}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
method="dialog"
|
|
||||||
className="flex flex-col gap-4 p-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target as HTMLFormElement);
|
|
||||||
const user = data.get("user") as string;
|
|
||||||
const password = data.get("password") as string;
|
|
||||||
console.log(user, password);
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "workers",
|
|
||||||
async () =>
|
|
||||||
fetchApi("workers/{workerId}", "PATCH", {
|
|
||||||
params: { workerId: workerId },
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
body: {
|
|
||||||
type: "application/json",
|
|
||||||
data: {
|
|
||||||
sdAuth: user && password ? { user, password } : null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).then(handleResponse),
|
|
||||||
{ populateCache: false },
|
|
||||||
);
|
|
||||||
dialogRef.current?.close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
User
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="text"
|
|
||||||
name="user"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="flex flex-1 flex-col items-stretch gap-1">
|
|
||||||
<span className="text-sm">
|
|
||||||
Password
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className="input-text"
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-outlined"
|
|
||||||
onClick={() => dialogRef.current?.close()}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<button type="submit" disabled={!sessionId} className="button-filled">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteWorkerDialog(props: {
|
|
||||||
dialogRef: RefObject<HTMLDialogElement>;
|
|
||||||
workerId: string;
|
|
||||||
sessionId: string | null;
|
|
||||||
}) {
|
|
||||||
const { dialogRef, workerId, sessionId } = props;
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog
|
|
||||||
className="dialog animate-pop-in backdrop-animate-fade-in"
|
|
||||||
ref={dialogRef}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
method="dialog"
|
|
||||||
className="flex flex-col gap-4 p-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
mutate(
|
|
||||||
(key) => Array.isArray(key) && key[0] === "workers",
|
|
||||||
async () =>
|
|
||||||
fetchApi("workers/{workerId}", "DELETE", {
|
|
||||||
params: { workerId: workerId },
|
|
||||||
query: { sessionId: sessionId! },
|
|
||||||
}).then(handleResponse),
|
|
||||||
{ populateCache: false },
|
|
||||||
);
|
|
||||||
dialogRef.current?.close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Are you sure you want to delete this worker?
|
|
||||||
</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" disabled={!sessionId} className="button-filled">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
34
ui/twind.ts
34
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,13 +76,13 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.link {
|
.link {
|
||||||
@apply text-sky-600 dark:text-sky-500 rounded-sm focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-sky-600 dark:focus:outline-sky-500;
|
@apply text-sky-600 dark:text-sky-500 rounded-sm focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-sky-600 dark:focus:outline-sky-500;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
|
@ -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,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