Files
cobble/src/bots-queue/push-bot-updated.ts
T
tbenandClaude Opus 5.5 926bad6bea Make bot types an extension of the bot's kind
Coordinator and worker come from bots.kind; bot_types now only holds
workspace_admin (was admin), each row naming its workspace, with no scope
field. Rows from v0.2.8 and earlier are fixed at boot by migrateBotTypes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:05:18 +01:00

111 lines
3.6 KiB
TypeScript

import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import grabBotAudience from "@/src/functions/backend/db/access/grab-bot-audience";
import { logError } from "@/src/functions/backend/logger";
import grabActiveBotTask from "@/src/functions/backend/db/bots/grab-active-bot-task";
import grabBotFace from "@/src/functions/backend/db/bots/grab-bot-face";
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
type Params = {
bot_id: number | string;
/**
* Only this user: a face change, which is theirs alone. Else everyone
* who sees the bot.
*/
user_id?: number | null;
/**
* Only these users (the connected ones, for the live relay).
*/
users?: Set<string>;
/**
* The computer the bot is on, when the push knows it (a switch, or the
* live relay). Null means its main one.
*/
computer_id?: number | null;
/**
* The workspaces the bot is in now, for a membership change: a
* workspace's crew drops a bot that's no longer in it.
*/
workspace_ids?: number[];
};
/**
* Pushes `server:bot-updated` to one user, or everyone who sees the bot
* (`grabBotAudience`), each with their own face, status line and active
* task with it, never another user's. Best effort: a push that fails is
* logged and never fails the change that caused it.
*/
export default async function pushBotUpdated({
bot_id,
user_id,
users,
computer_id,
workspace_ids,
}: Params) {
if (!bot_id) return;
try {
await push({ bot_id, user_id, users, computer_id, workspace_ids });
} catch (error) {
await logError({
title: "Bot update wasn't pushed",
error,
metadata: { bot_id },
}).catch(() => {});
}
}
async function push({
bot_id,
user_id,
users,
computer_id,
workspace_ids,
}: Params) {
const audience = user_id ? [user_id] : await grabBotAudience({ bot_id });
/**
* Its colour, stone and picture, so a recolour or a new face reaches
* every open tab.
*/
const identity = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: `SELECT color, stone, image_media_id FROM bots WHERE id = ? LIMIT 1`,
values: [bot_id],
})
).payload?.[0];
/**
* The workspaces it leads, so each crew shows its shield.
*/
const admin_workspace_ids = (
(
await BunSQLite.sql<{ workspace_id: number }>({
sql: `SELECT workspace_id FROM bot_types WHERE bot_id = ? AND bot_type = 'workspace_admin' AND workspace_id IS NOT NULL`,
values: [bot_id],
})
).payload || []
).map((row) => Number(row.workspace_id));
for (const viewer_id of audience) {
if (users && !users.has(String(viewer_id))) continue;
sendToUser({
user_id: viewer_id,
data: {
event: "server:bot-updated",
data: {
id: Number(bot_id),
...(await grabBotFace({ bot_id, user_id: viewer_id })),
color: identity?.color || null,
stone: identity?.stone || null,
image_media_id: identity?.image_media_id ? Number(identity.image_media_id) : null,
admin_workspace_ids,
...(computer_id !== undefined ? { computer_id: computer_id || null } : {}),
...(workspace_ids !== undefined ? { workspace_ids } : {}),
task: await grabActiveBotTask({ bot_id, user_id: viewer_id }),
},
},
});
}
}