Share one crew across the install, keep user memory per user
Bots, workspaces and computers now belong to the install. Users reach workers only through bot_users (assigned by a super admin or admin) and workspace_users; the coordinator and main computer are one per install, and only the super admin sees the coordinator. Conversations are per user (bot_sessions.user_id), jobs run for the session's user, and live updates go only to who should see them. A bot's core memory is now general knowledge shared by every user it works for; what it learns about a user lives in bot_user_memories. One-off scripts move existing installs over: move-bot-memory-to-owners (run first), then share-crew. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabCrewRole from "./grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws unless the user manages the crew (a super admin or an admin).
|
||||
* Returns their role.
|
||||
*/
|
||||
export default async function assertCrewManager({ user_id }: Params) {
|
||||
const role = await grabCrewRole({ user_id });
|
||||
|
||||
if (role == "member") {
|
||||
throw new AppError(`Only an admin can do that.`);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Every user who sees a bot (`grabUserBot`), for live updates about it:
|
||||
* the super admin, and for a worker the admins, the users it's assigned
|
||||
* to and the members of its workspaces.
|
||||
*/
|
||||
export default async function grabBotAudience({ bot_id }: Params): Promise<number[]> {
|
||||
const res = await BunSQLite.sql<{ user_id: number }>({
|
||||
sql: `SELECT user_id FROM user_types WHERE user_type = 'super_admin'
|
||||
UNION
|
||||
SELECT ut.user_id FROM user_types ut JOIN bots b ON b.id = ? AND b.kind != 'coordinator'
|
||||
WHERE ut.user_type = 'admin'
|
||||
UNION
|
||||
SELECT bu.user_id FROM bot_users bu JOIN bots b ON b.id = bu.bot_id AND b.kind != 'coordinator'
|
||||
WHERE bu.bot_id = ?
|
||||
UNION
|
||||
SELECT wu.user_id FROM workspace_users wu
|
||||
JOIN workspace_bots wb ON wb.workspace_id = wu.workspace_id AND wb.bot_id = ?
|
||||
JOIN workspaces w ON w.id = wu.workspace_id AND w.archived = 0
|
||||
JOIN bots b ON b.id = wb.bot_id AND b.kind != 'coordinator'`,
|
||||
values: [bot_id, bot_id, bot_id],
|
||||
});
|
||||
|
||||
return (res.payload || []).map((row) => Number(row.user_id)).filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
computer_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Every user who sees a computer (`computersSql`), for live updates about
|
||||
* it: super admins and admins, and the users whose workspaces or assigned
|
||||
* workers use it.
|
||||
*/
|
||||
export default async function grabComputerAudience({ computer_id }: Params): Promise<number[]> {
|
||||
const res = await BunSQLite.sql<{ user_id: number }>({
|
||||
sql: `SELECT user_id FROM user_types WHERE user_type IN ('super_admin', 'admin')
|
||||
UNION
|
||||
SELECT wu.user_id FROM workspace_users wu
|
||||
JOIN workspace_computers wc ON wc.workspace_id = wu.workspace_id AND wc.computer_id = ?
|
||||
UNION
|
||||
SELECT bu.user_id FROM bot_users bu
|
||||
JOIN bot_computers bc ON bc.bot_id = bu.bot_id AND bc.computer_id = ?
|
||||
JOIN bots b ON b.id = bu.bot_id AND b.kind != 'coordinator'`,
|
||||
values: [computer_id, computer_id],
|
||||
});
|
||||
|
||||
return (res.payload || []).map((row) => Number(row.user_id)).filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
|
||||
/**
|
||||
* Every worker and every workspace (not archived), for picking what a
|
||||
* user may reach. Never the coordinator: it's the super admin's alone.
|
||||
*/
|
||||
export default async function grabCrewOptions() {
|
||||
const [bots, workspaces] = await Promise.all([
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT id, name, role FROM bots WHERE kind = 'worker' ORDER BY name COLLATE NOCASE ASC, id ASC`,
|
||||
}),
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT id, name FROM workspaces WHERE archived = 0 ORDER BY name COLLATE NOCASE ASC, id ASC`,
|
||||
}),
|
||||
]);
|
||||
|
||||
return { crew_bots: bots.payload || [], crew_workspaces: workspaces.payload || [] };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import crewRole from "@/src/utils/rbac/crew-role";
|
||||
import grabUserTypes from "../users/grab-user-types";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A user's `crewRole`, read from their user types.
|
||||
*/
|
||||
export default async function grabCrewRole({ user_id }: Params) {
|
||||
return crewRole({ user_types: await grabUserTypes({ user_id }) });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_MEDIA, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserWorkspace from "../workspaces/grab-user-workspace";
|
||||
|
||||
type Params = {
|
||||
media_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot's or workspace's picture (private media, uploaded by whoever set
|
||||
* it) for a user who reaches that bot or workspace, or null. Bots and
|
||||
* workspaces belong to the install, so their pictures show to everyone
|
||||
* who sees them.
|
||||
*/
|
||||
export default async function grabSharedPictureMedia({ media_id, user_id }: Params) {
|
||||
const [bot, workspace] = await Promise.all([
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT id FROM bots WHERE image_media_id = ? LIMIT 1`,
|
||||
values: [media_id],
|
||||
}).then((res) => res.payload?.[0]),
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT id FROM workspaces WHERE image_media_id = ? LIMIT 1`,
|
||||
values: [media_id],
|
||||
}).then((res) => res.payload?.[0]),
|
||||
]);
|
||||
|
||||
const allowed =
|
||||
(bot?.id && (await grabUserBot({ bot_id: bot.id, user_id }))?.id) ||
|
||||
(workspace?.id && (await grabUserWorkspace({ workspace_id: workspace.id, user_id }))?.id);
|
||||
|
||||
if (!allowed) return null;
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_MEDIA>({
|
||||
sql: `SELECT * FROM media WHERE id = ? LIMIT 1`,
|
||||
values: [media_id],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
/**
|
||||
* Every worker assigned to them, replacing the old list. The
|
||||
* coordinator is never assigned. Left out: unchanged.
|
||||
*/
|
||||
bot_ids?: number[];
|
||||
/**
|
||||
* Every workspace they're in, replacing the old list. Left out:
|
||||
* unchanged.
|
||||
*/
|
||||
workspace_ids?: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets which workers a user chats with in their own chat (`bot_users`)
|
||||
* and which workspaces they work in (`workspace_users`). The caller checks
|
||||
* the actor manages the crew. Ids that aren't a worker or a workspace are
|
||||
* skipped.
|
||||
*/
|
||||
export default async function setUserAccess({ user_id, bot_ids, workspace_ids }: Params) {
|
||||
const now = Date.now();
|
||||
|
||||
if (bot_ids) {
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bot_users WHERE user_id = ?`, values: [user_id] });
|
||||
|
||||
for (const bot_id of new Set(bot_ids)) {
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO bot_users (created_at, updated_at, bot_id, user_id)
|
||||
SELECT ?, ?, id, ? FROM bots WHERE id = ? AND kind = 'worker'`,
|
||||
values: [now, now, user_id, bot_id],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (workspace_ids) {
|
||||
await BunSQLite.sql({ sql: `DELETE FROM workspace_users WHERE user_id = ?`, values: [user_id] });
|
||||
|
||||
for (const workspace_id of new Set(workspace_ids)) {
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO workspace_users (created_at, updated_at, workspace_id, user_id)
|
||||
SELECT ?, ?, id, ? FROM workspaces WHERE id = ?`,
|
||||
values: [now, now, user_id, workspace_id],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import type {
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -14,10 +18,11 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts an active session in a conversation. Callers close the old one
|
||||
* Starts an active session in a user's conversation. Callers close the old one
|
||||
* first (`startBotSession`).
|
||||
*/
|
||||
export default async function createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id,
|
||||
@@ -25,6 +30,7 @@ export default async function createBotSession({
|
||||
const now = Date.now();
|
||||
|
||||
const row: BUN_SQLITE_COBBLE_BOT_SESSIONS = {
|
||||
user_id: Number(user_id),
|
||||
bot_id: Number(bot_id),
|
||||
workspace_id: workspace_id ? Number(workspace_id) : undefined,
|
||||
previous_session_id: previous_session_id || undefined,
|
||||
|
||||
@@ -6,6 +6,10 @@ import grabActiveBotSession from "./grab-active-bot-session";
|
||||
import startBotSession from "./start-bot-session";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -17,16 +21,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation's active session, started first when there is none
|
||||
* The user's conversation's active session, started first when there is none
|
||||
* (carrying on from its latest closed one) or, with `rollover`, when the
|
||||
* active one is full.
|
||||
*/
|
||||
export default async function ensureBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
rollover,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS> {
|
||||
const active = await grabActiveBotSession({ bot_id, workspace_id });
|
||||
const active = await grabActiveBotSession({ user_id, bot_id, workspace_id });
|
||||
|
||||
if (active?.id) {
|
||||
if (!rollover) return active;
|
||||
@@ -43,18 +48,19 @@ export default async function ensureBotSession({
|
||||
|
||||
return count < AppData.BotSessionMaxMessages
|
||||
? active
|
||||
: await startBotSession({ bot_id, workspace_id });
|
||||
: await startBotSession({ user_id, bot_id, workspace_id });
|
||||
}
|
||||
|
||||
const latest = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT id FROM bot_sessions WHERE bot_id = ? AND workspace_id IS ?
|
||||
sql: `SELECT id FROM bot_sessions WHERE user_id = ? AND bot_id = ? AND workspace_id IS ?
|
||||
ORDER BY last_activity_at DESC, id DESC LIMIT 1`,
|
||||
values: [bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
values: [user_id, bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
return await createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id: latest?.id || null,
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { BUN_SQLITE_COBBLE_BOT_SESSIONS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* Null or undefined: the bot's own chat.
|
||||
@@ -10,18 +14,19 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The active session of one conversation (a bot in its own chat or in one
|
||||
* workspace), or null when it has none yet.
|
||||
* The active session of one user's conversation (with a bot in its own
|
||||
* chat or in one workspace), or null when it has none yet.
|
||||
*/
|
||||
export default async function grabActiveBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS | null> {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT * FROM bot_sessions
|
||||
WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
ORDER BY id DESC LIMIT 1`,
|
||||
values: [bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
values: [user_id, bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -3,6 +3,10 @@ import { AppData } from "@/src/data/app-data";
|
||||
import type { BotSessionListItem } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -13,11 +17,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A conversation's sessions for the sessions modal, latest activity first,
|
||||
* A user's conversation's sessions for the sessions modal, latest activity first,
|
||||
* each with its message count and latest message. Empty sessions are left
|
||||
* out unless active.
|
||||
*/
|
||||
export default async function grabBotSessions({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
query,
|
||||
@@ -33,7 +38,7 @@ export default async function grabBotSessions({
|
||||
sql += ` (SELECT COUNT(*) FROM bot_messages m WHERE m.session_id = s.id) AS message_count,\n`;
|
||||
sql += ` (SELECT m.text FROM bot_messages m WHERE m.session_id = s.id ORDER BY m.created_at DESC, m.id DESC LIMIT 1) AS last_message_text\n`;
|
||||
sql += `FROM bot_sessions s\n`;
|
||||
sql += `WHERE s.bot_id = ? AND s.workspace_id IS ?\n`;
|
||||
sql += `WHERE s.user_id = ? AND s.bot_id = ? AND s.workspace_id IS ?\n`;
|
||||
sql += `AND (s.status = 'active' OR EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = s.id))\n`;
|
||||
|
||||
if (like) {
|
||||
@@ -47,6 +52,7 @@ export default async function grabBotSessions({
|
||||
const res = await BunSQLite.sql<BotSessionListItem>({
|
||||
sql,
|
||||
values: [
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id ? Number(workspace_id) : null,
|
||||
...(like ? [like, like, like] : []),
|
||||
|
||||
@@ -24,7 +24,7 @@ type Params = {
|
||||
* `/admin/workspaces/<id>/bots/<id>`): the rail's crew (the workspace's
|
||||
* in a workspace) and workspaces, the bot, its active session (marked
|
||||
* read) and its messages, its models, computers and active task there.
|
||||
* Null when the bot isn't the user's or isn't in the workspace.
|
||||
* Null when the user can't chat with the bot there.
|
||||
*/
|
||||
export default async function grabConversationProps({
|
||||
user_id,
|
||||
@@ -38,7 +38,7 @@ export default async function grabConversationProps({
|
||||
if (!conversation) return null;
|
||||
|
||||
const scope = conversation.workspace_id;
|
||||
const session = await ensureBotSession({ bot_id: conversation.bot.id, workspace_id: scope });
|
||||
const session = await ensureBotSession({ user_id, bot_id: conversation.bot.id, workspace_id: scope });
|
||||
|
||||
if (session.unread) {
|
||||
await markBotSessionRead({ session_id: session.id!, user_id });
|
||||
@@ -71,7 +71,7 @@ export default async function grabConversationProps({
|
||||
workspace_id: scope || undefined,
|
||||
})
|
||||
).catch(() => null),
|
||||
grabBotComputers({ bot_id: bot.id, user_id, workspace_id: scope }),
|
||||
grabBotComputers({ bot_id: bot.id, workspace_id: scope }),
|
||||
grabActiveBotTask({ bot_id: bot.id, session_id: session.id }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,15 +7,14 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A session only if its bot belongs to the given owner.
|
||||
* A session only if it's the given user's own.
|
||||
*/
|
||||
export default async function grabUserBotSession({
|
||||
session_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS | null> {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT s.* FROM bot_sessions s JOIN bots b ON b.id = s.bot_id
|
||||
WHERE s.id = ? AND b.user_id = ? LIMIT 1`,
|
||||
sql: `SELECT s.* FROM bot_sessions s WHERE s.id = ? AND s.user_id = ? LIMIT 1`,
|
||||
values: [session_id, user_id],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabChatBot from "../bots/grab-chat-bot";
|
||||
import grabWorkspaceBot from "../workspaces/grab-workspace-bot";
|
||||
|
||||
type Params = {
|
||||
@@ -19,9 +19,9 @@ export type UserConversation = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One of the user's conversations: their bot, in its own chat or in one of
|
||||
* their workspaces it works in. Throws when the bot isn't theirs or isn't
|
||||
* in that workspace.
|
||||
* One of the user's conversations: a bot they chat with in its own chat
|
||||
* (`grabChatBot`) or in a workspace they're in (`grabWorkspaceBot`).
|
||||
* Throws when they can't.
|
||||
*/
|
||||
export default async function grabUserConversation({
|
||||
user_id,
|
||||
@@ -42,7 +42,7 @@ export default async function grabUserConversation({
|
||||
};
|
||||
}
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const bot = await grabChatBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found`);
|
||||
|
||||
@@ -6,12 +6,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears a session's unread flag when its owner has the conversation open.
|
||||
* Clears a session's unread flag when its user has the conversation open.
|
||||
*/
|
||||
export default async function markBotSessionRead({ session_id, user_id }: Params) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET unread = 0
|
||||
WHERE id = ? AND unread = 1 AND bot_id IN (SELECT id FROM bots WHERE user_id = ?)`,
|
||||
WHERE id = ? AND unread = 1 AND user_id = ?`,
|
||||
values: [session_id, user_id],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ export default async function reopenBotSession({
|
||||
const workspace_id = session.workspace_id || null;
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM bot_sessions WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
sql: `DELETE FROM bot_sessions WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
AND NOT EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = bot_sessions.id)`,
|
||||
values: [session.bot_id!, workspace_id],
|
||||
values: [session.user_id!, session.bot_id!, workspace_id],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET status = 'closed', unread = 0, updated_at = ?
|
||||
WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'`,
|
||||
values: [now, session.bot_id!, workspace_id],
|
||||
WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'`,
|
||||
values: [now, session.user_id!, session.bot_id!, workspace_id],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
|
||||
@@ -4,6 +4,10 @@ import createBotSession from "./create-bot-session";
|
||||
import grabActiveBotSession from "./grab-active-bot-session";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
@@ -14,10 +18,11 @@ type Params = {
|
||||
* An active session with no messages yet is kept as it is.
|
||||
*/
|
||||
export default async function startBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS> {
|
||||
const active = await grabActiveBotSession({ bot_id, workspace_id });
|
||||
const active = await grabActiveBotSession({ user_id, bot_id, workspace_id });
|
||||
|
||||
if (active?.id) {
|
||||
const has_messages = (
|
||||
@@ -36,6 +41,7 @@ export default async function startBotSession({
|
||||
}
|
||||
|
||||
return await createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id: active?.id || null,
|
||||
|
||||
@@ -9,7 +9,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels one of the user's pending or running jobs. It's marked failed
|
||||
* Cancels a pending or running job in one of the user's conversations. It's marked failed
|
||||
* with `BotJobCancelledError`; a running handler finds out on its next
|
||||
* `updateBotJob` (which returns false) and stops. Its task is closed.
|
||||
*/
|
||||
@@ -19,7 +19,7 @@ export default async function cancelBotJob({ user_id, job_id }: Params) {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS_QUEUE>({
|
||||
sql: `UPDATE bots_queue SET status = 'failed', last_error = ?, locked_at = NULL, locked_by = NULL, finished_at = ?, updated_at = ?
|
||||
WHERE id = ? AND status IN ('pending', 'processing')
|
||||
AND bot_id IN (SELECT id FROM bots WHERE user_id = ?)
|
||||
AND session_id IN (SELECT id FROM bot_sessions WHERE user_id = ?)
|
||||
RETURNING *`,
|
||||
values: [BotJobCancelledError, now, now, job_id, user_id],
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ export default async function enqueueBotJob({
|
||||
}
|
||||
|
||||
if (computer_id) {
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, user_id, workspace_id });
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id });
|
||||
|
||||
if (!computers.some((c) => String(c.id) == String(computer_id))) {
|
||||
throw new AppError(`This bot has no access to that computer.`);
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS_QUEUE } from "@/db/types/db";
|
||||
import ensureBotSession from "../bot-sessions/ensure-bot-session";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
|
||||
type Params = {
|
||||
job: Pick<BUN_SQLITE_COBBLE_BOTS_QUEUE, "bot_id" | "session_id" | "workspace_id">;
|
||||
job: Pick<BUN_SQLITE_COBBLE_BOTS_QUEUE, "session_id">;
|
||||
};
|
||||
|
||||
/**
|
||||
* The session a job's result goes in: its own, or (for a job queued
|
||||
* without one) the active session of its bot's conversation there.
|
||||
* The session a job's result goes in. Every job is queued in one
|
||||
* (`enqueueBotJob`), which says whose conversation it is.
|
||||
*/
|
||||
export default async function jobSessionId({ job }: Params) {
|
||||
if (job.session_id) return Number(job.session_id);
|
||||
export default function jobSessionId({ job }: Params) {
|
||||
if (!job.session_id) {
|
||||
throw new AppError(`This job has no conversation.`);
|
||||
}
|
||||
|
||||
const session = await ensureBotSession({
|
||||
bot_id: job.bot_id!,
|
||||
workspace_id: job.workspace_id || null,
|
||||
});
|
||||
|
||||
return Number(session.id);
|
||||
return Number(job.session_id);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ type Params = {
|
||||
/**
|
||||
* Reports a handed-over job back to the bot that handed it over
|
||||
* (`requested_by_bot_id`, the coordinator): a message in the conversation
|
||||
* it was handed over from (the active session there, which may have moved
|
||||
* on since), with the worker's report, flagged needs-you when the job
|
||||
* it was handed over from (that user's active session there, which may
|
||||
* have moved on since), with the worker's report, flagged needs-you when the job
|
||||
* failed. Returns the message for the live push, or null when nobody
|
||||
* handed the job over.
|
||||
*/
|
||||
@@ -48,14 +48,16 @@ export default async function relayJobToRequester({ job, text, failed }: Params)
|
||||
const requested_from = job.requested_by_session_id
|
||||
? (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT workspace_id FROM bot_sessions WHERE id = ? AND bot_id = ? LIMIT 1`,
|
||||
sql: `SELECT user_id, workspace_id FROM bot_sessions WHERE id = ? AND bot_id = ? LIMIT 1`,
|
||||
values: [job.requested_by_session_id, requester.id],
|
||||
})
|
||||
).payload?.[0]
|
||||
: null;
|
||||
|
||||
const workspace_id = requested_from ? requested_from.workspace_id || null : job.workspace_id || null;
|
||||
const session = await ensureBotSession({ bot_id: requester.id, workspace_id });
|
||||
if (!requested_from?.user_id) return null;
|
||||
|
||||
const workspace_id = requested_from.workspace_id || null;
|
||||
const session = await ensureBotSession({ user_id: requested_from.user_id, bot_id: requester.id, workspace_id });
|
||||
|
||||
const name = worker?.name || "A crew member";
|
||||
const title = job.title || "the job";
|
||||
|
||||
@@ -2,8 +2,12 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BunSQLiteTables } from "@/db/types/db";
|
||||
import type { BotHue } from "@/src/dict/bot-dict";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who creates it (a super admin or admin). Bots belong to the install.
|
||||
*/
|
||||
user_id: number | string;
|
||||
name: string;
|
||||
role?: string;
|
||||
@@ -15,11 +19,15 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a worker bot to the user's crew and returns its id. It has no
|
||||
* Adds a worker bot to the install and returns its id. Nobody but super
|
||||
* admins and admins reaches it until it's assigned to a user or added to
|
||||
* a workspace. It has no
|
||||
* computer of its own until it's given one; in a workspace it uses the
|
||||
* workspace's.
|
||||
*/
|
||||
export default async function createBot({ user_id, name, role, hue, system_prompt }: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
assertPromptLength({ label: "Instructions", text: system_prompt });
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -14,6 +15,8 @@ type Params = {
|
||||
* its row. The coordinator can't be deleted.
|
||||
*/
|
||||
export default async function deleteBot({ user_id, bot_id }: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
|
||||
@@ -4,12 +4,16 @@ import { CoordinatorBotName } from "@/src/dict/bot-dict";
|
||||
import { CoordinatorSystemPrompt } from "@/src/data/coordinator-system-prompt";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who it's created by when it's missing (the super admin).
|
||||
*/
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the id of the user's coordinator, creating it first (with the
|
||||
* default `CoordinatorSystemPrompt`) if the user has none. A
|
||||
* Returns the id of the install's one coordinator (the super admin's
|
||||
* alone), creating it first (with the default `CoordinatorSystemPrompt`)
|
||||
* if there is none. A
|
||||
* coordinator without a prompt gets the default; an edited prompt is never
|
||||
* overwritten. Safe to call on every visit.
|
||||
*/
|
||||
@@ -22,7 +26,6 @@ export default async function ensureCoordinatorBot({ user_id }: Params) {
|
||||
table: "bots",
|
||||
query: {
|
||||
query: {
|
||||
user_id: { value: user_id },
|
||||
kind: { value: "coordinator" },
|
||||
},
|
||||
order: { field: "id", strategy: "ASC" },
|
||||
|
||||
@@ -12,21 +12,28 @@ type Params = {
|
||||
* Only a task started from this session.
|
||||
*/
|
||||
session_id?: number | string | null;
|
||||
/**
|
||||
* Only a task in this user's conversations.
|
||||
*/
|
||||
user_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bot's most recent active task (in one session, when given), its
|
||||
* The bot's most recent active task (in one session or one user's
|
||||
* conversations, when given), its
|
||||
* steps and what its job has done so far, for the working-on panel.
|
||||
*/
|
||||
export default async function grabActiveBotTask({
|
||||
bot_id,
|
||||
session_id,
|
||||
user_id,
|
||||
}: Params): Promise<BotTaskWithSteps | null> {
|
||||
const task_res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_TASKS>({
|
||||
sql: `SELECT * FROM bot_tasks WHERE bot_id = ? AND status = 'active'
|
||||
${session_id ? `AND session_id = ?` : ""}
|
||||
${user_id ? `AND session_id IN (SELECT id FROM bot_sessions WHERE user_id = ?)` : ""}
|
||||
ORDER BY created_at DESC, id DESC LIMIT 1`,
|
||||
values: session_id ? [bot_id, session_id] : [bot_id],
|
||||
values: [bot_id, ...(session_id ? [session_id] : []), ...(user_id ? [user_id] : [])],
|
||||
});
|
||||
|
||||
const task = task_res.singleRes || task_res.payload?.[0];
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import composeBotPrompt from "@/src/utils/compose-bot-prompt";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabWorkspaceBot from "../workspaces/grab-workspace-bot";
|
||||
import grabBotUserMemory from "./grab-bot-user-memory";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The user the bot works with: only their memory joins the prompt.
|
||||
*/
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The workspace the bot is working in. Its context and the bot's
|
||||
@@ -15,8 +19,8 @@ type Params = {
|
||||
|
||||
/**
|
||||
* The full prompt for one of the user's bots (`composeBotPrompt`): its
|
||||
* core instructions and memory, and in a workspace that workspace's
|
||||
* context and what the bot has for it.
|
||||
* core instructions and memory, what it remembers about this user, and in
|
||||
* a workspace that workspace's context and what the bot has for it.
|
||||
*/
|
||||
export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: Params) {
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
@@ -36,6 +40,7 @@ export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: P
|
||||
return composeBotPrompt({
|
||||
system_prompt: bot.system_prompt,
|
||||
memory: bot.memory,
|
||||
user_memory: await grabBotUserMemory({ bot_id: bot.id, user_id }),
|
||||
workspace: found?.workspace,
|
||||
workspace_bot: found?.membership,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOT_USER_MEMORIES } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a bot remembers about one user, or null when it has nothing yet.
|
||||
* The caller checks the user may talk to the bot.
|
||||
*/
|
||||
export default async function grabBotUserMemory({ bot_id, user_id }: Params) {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_USER_MEMORIES>({
|
||||
sql: `SELECT memory FROM bot_user_memories WHERE bot_id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [bot_id, user_id],
|
||||
});
|
||||
|
||||
return res.payload?.[0]?.memory || null;
|
||||
}
|
||||
@@ -1,26 +1,33 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import workspaceBotsSql from "@/src/utils/crew-access/workspace-bots-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
/**
|
||||
* A workspace's crew instead: the coordinator and the bots in it, with
|
||||
* their conversations in that workspace.
|
||||
* A workspace's crew instead: the bots the user chats with there
|
||||
* (check they're in the workspace first), with their conversations
|
||||
* there.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A user's crew for the rail: the coordinator pinned first, then
|
||||
* workers by latest activity. Each row carries its conversation's unread
|
||||
* flag, latest activity and latest message: in its own chat, or in the
|
||||
* workspace when one is given.
|
||||
* The bots a user chats with, for the rail: in their own chat
|
||||
* (`chatBotsSql`) or in one workspace (`workspaceBotsSql`). The
|
||||
* coordinator is pinned first, then workers by latest activity. Each row
|
||||
* carries the user's own conversation there: its unread flag, latest
|
||||
* activity and latest message.
|
||||
*/
|
||||
export default async function grabBots({
|
||||
user_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BotWithLastMessage[]> {
|
||||
const scope = workspace_id ? Number(workspace_id) : null;
|
||||
const role = await grabCrewRole({ user_id });
|
||||
const access = scope ? workspaceBotsSql({ role, workspace_id: scope }) : chatBotsSql({ role, user_id });
|
||||
|
||||
let sql = ``;
|
||||
|
||||
@@ -33,27 +40,22 @@ export default async function grabBots({
|
||||
sql += `FROM bots b\n`;
|
||||
sql += `LEFT JOIN (\n`;
|
||||
sql += ` SELECT bot_id, MAX(unread) AS unread, MAX(last_activity_at) AS last_activity_at\n`;
|
||||
sql += ` FROM bot_sessions WHERE workspace_id IS ? GROUP BY bot_id\n`;
|
||||
sql += ` FROM bot_sessions WHERE workspace_id IS ? AND user_id = ? GROUP BY bot_id\n`;
|
||||
sql += `) t ON t.bot_id = b.id\n`;
|
||||
sql += `LEFT JOIN bot_messages lm ON lm.id = (\n`;
|
||||
sql += ` SELECT id FROM bot_messages\n`;
|
||||
sql += ` WHERE bot_id = b.id AND workspace_id IS ?\n`;
|
||||
sql += ` ORDER BY created_at DESC, id DESC\n`;
|
||||
sql += ` SELECT m.id FROM bot_messages m JOIN bot_sessions s ON s.id = m.session_id\n`;
|
||||
sql += ` WHERE m.bot_id = b.id AND m.workspace_id IS ? AND s.user_id = ?\n`;
|
||||
sql += ` ORDER BY m.created_at DESC, m.id DESC\n`;
|
||||
sql += ` LIMIT 1\n`;
|
||||
sql += `)\n`;
|
||||
sql += `WHERE b.user_id = ?\n`;
|
||||
|
||||
if (scope) {
|
||||
sql += `AND (b.kind = 'coordinator' OR b.id IN (SELECT bot_id FROM workspace_bots WHERE workspace_id = ?))\n`;
|
||||
}
|
||||
|
||||
sql += `WHERE ${access.sql}\n`;
|
||||
sql += `ORDER BY\n`;
|
||||
sql += ` CASE WHEN b.kind = 'coordinator' THEN 0 ELSE 1 END,\n`;
|
||||
sql += ` COALESCE(lm.created_at, t.last_activity_at, b.created_at) DESC\n`;
|
||||
|
||||
const res = await BunSQLite.sql<BotWithLastMessage>({
|
||||
sql,
|
||||
values: [scope, scope, user_id, ...(scope ? [scope] : [])],
|
||||
values: [scope, user_id, scope, user_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if the user chats with it in its own chat (`chatBotsSql`).
|
||||
*/
|
||||
export default async function grabChatBot({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
|
||||
const access = chatBotsSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [bot_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
@@ -7,15 +9,23 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if it belongs to the given owner.
|
||||
* A bot only if the user reaches it somewhere: in its own chat
|
||||
* (`chatBotsSql`) or in a workspace they're in. Super admins and admins
|
||||
* reach every worker; only the super admin reaches the coordinator.
|
||||
*/
|
||||
export default async function grabUserBot({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
|
||||
const access = chatBotsSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [bot_id, user_id],
|
||||
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND (${access.sql} OR (b.kind != 'coordinator' AND b.id IN (
|
||||
SELECT wb.bot_id FROM workspace_bots wb
|
||||
JOIN workspace_users wu ON wu.workspace_id = wb.workspace_id AND wu.user_id = ?
|
||||
JOIN workspaces w ON w.id = wb.workspace_id AND w.archived = 0
|
||||
))) LIMIT 1`,
|
||||
values: [bot_id, ...access.values, user_id],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The whole new memory about this user (markdown), replacing the old one.
|
||||
*/
|
||||
memory: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces what a bot remembers about one user. Only that user's runs see
|
||||
* it; the bot's core memory stays the same for everyone.
|
||||
*/
|
||||
export default async function updateBotUserMemory({
|
||||
user_id,
|
||||
bot_id,
|
||||
memory,
|
||||
}: Params) {
|
||||
assertPromptLength({ label: "Memory", text: memory });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `INSERT INTO bot_user_memories (created_at, updated_at, bot_id, user_id, memory)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(bot_id, user_id) DO UPDATE SET updated_at = excluded.updated_at, memory = excluded.memory`,
|
||||
values: [now, now, bot.id, user_id, memory],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the bot's memory: ${res.error}`);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { BotHue } from "@/src/dict/bot-dict";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -19,6 +20,8 @@ type Params = {
|
||||
* change. Returns the saved bot.
|
||||
*/
|
||||
export default async function updateBot({ user_id, bot_id, name, role, hue, system_prompt }: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
assertPromptLength({ label: "Instructions", text: system_prompt });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserComputer from "./grab-user-computer";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -18,6 +19,8 @@ export default async function addBotToComputer({
|
||||
bot_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const computer = await grabUserComputer({ computer_id, user_id });
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ import grabEnv from "../envs/grab-env";
|
||||
import provisionComputer from "./provision-computer";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who creates it. Computers belong to the install.
|
||||
*/
|
||||
user_id: number | string;
|
||||
name: string;
|
||||
description?: string;
|
||||
/**
|
||||
* `main` only for the owner's one main computer (`ensureMainComputer`).
|
||||
* `main` only for the install's one main computer (`ensureMainComputer`).
|
||||
*/
|
||||
kind?: ComputerKind;
|
||||
/**
|
||||
@@ -29,7 +32,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a new computer for the user and its container
|
||||
* Records a new computer and its container
|
||||
* (`<AppData.ComputerContainerPrefix>-<id>`), then sets the container up
|
||||
* in the background (`provisionComputer`). Returns the computer at once,
|
||||
* still `creating`, and the setup to await when the caller must (a CLI
|
||||
|
||||
@@ -10,9 +10,12 @@ import createComputer from "./create-computer";
|
||||
import provisionComputer from "./provision-computer";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who it's created by when it's missing (the super admin).
|
||||
*/
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The owner's coordinator (from `ensureCoordinatorBot`). It always has
|
||||
* The coordinator (from `ensureCoordinatorBot`). It always has
|
||||
* access to the main computer.
|
||||
*/
|
||||
coordinator_id: number | string;
|
||||
@@ -24,7 +27,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the id of the user's main computer, creating it first if
|
||||
* Returns the id of the install's one main computer, creating it first if
|
||||
* missing (`createComputer`, which sets its container up in the
|
||||
* background), and makes sure the coordinator has access to it. A main
|
||||
* computer recorded before the app managed containers (no docker id, no
|
||||
@@ -44,7 +47,6 @@ export default async function ensureMainComputer({
|
||||
table: "computers",
|
||||
query: {
|
||||
query: {
|
||||
user_id: { value: user_id },
|
||||
kind: { value: "main" },
|
||||
},
|
||||
order: { field: "id", strategy: "ASC" },
|
||||
|
||||
@@ -3,7 +3,6 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The workspace the bot is working in: then its computers are the
|
||||
* workspace's, the default first, and nothing else.
|
||||
@@ -15,27 +14,26 @@ type Params = {
|
||||
* Every computer a bot can use where it's working. In a workspace, the
|
||||
* workspace's computers (none linked: none at all), its default first. In
|
||||
* its own chat, the ones it was given directly (`bot_computers`); the
|
||||
* coordinator can use all of the owner's. There the one it works on now
|
||||
* (`bots.computer_id`) comes first, else its main one, so `computers[0]`
|
||||
* is where its tasks run by default.
|
||||
* coordinator can use all of them. There the one it works on now
|
||||
* (`bots.computer_id`) comes first, else the main one, so `computers[0]`
|
||||
* is where its tasks run by default. The same for every user it works
|
||||
* for: callers check the user reaches the bot.
|
||||
*/
|
||||
export default async function grabBotComputers({
|
||||
bot_id,
|
||||
user_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_COMPUTERS[]> {
|
||||
if (workspace_id) {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT c.* FROM computers c
|
||||
JOIN workspace_computers wc ON wc.computer_id = c.id AND wc.workspace_id = ?
|
||||
JOIN workspaces w ON w.id = wc.workspace_id AND w.user_id = c.user_id AND w.archived = 0
|
||||
WHERE c.user_id = ?
|
||||
AND (
|
||||
(SELECT kind FROM bots WHERE id = ? AND user_id = c.user_id) = 'coordinator'
|
||||
JOIN workspaces w ON w.id = wc.workspace_id AND w.archived = 0
|
||||
WHERE (
|
||||
(SELECT kind FROM bots WHERE id = ?) = 'coordinator'
|
||||
OR ? IN (SELECT bot_id FROM workspace_bots WHERE workspace_id = w.id)
|
||||
)
|
||||
ORDER BY c.id = w.computer_id DESC, wc.id ASC`,
|
||||
values: [workspace_id, user_id, bot_id, Number(bot_id)],
|
||||
values: [workspace_id, bot_id, Number(bot_id)],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
@@ -43,14 +41,13 @@ export default async function grabBotComputers({
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT c.* FROM computers c
|
||||
WHERE c.user_id = ?
|
||||
AND (
|
||||
(SELECT kind FROM bots WHERE id = ? AND user_id = c.user_id) = 'coordinator'
|
||||
WHERE (
|
||||
(SELECT kind FROM bots WHERE id = ?) = 'coordinator'
|
||||
OR c.id IN (SELECT computer_id FROM bot_computers WHERE bot_id = ?)
|
||||
)
|
||||
ORDER BY c.id = (SELECT computer_id FROM bots WHERE id = ?) DESC,
|
||||
c.kind = 'main' DESC, c.id ASC`,
|
||||
values: [user_id, bot_id, bot_id, bot_id],
|
||||
values: [bot_id, bot_id, bot_id],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import computersSql from "@/src/utils/crew-access/computers-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
computer_id: number | string;
|
||||
@@ -7,15 +9,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A computer only if it belongs to the given owner.
|
||||
* A computer only if the user sees it (`computersSql`).
|
||||
*/
|
||||
export default async function grabUserComputer({
|
||||
computer_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_COMPUTERS | null> {
|
||||
const access = computersSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT * FROM computers WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [computer_id, user_id],
|
||||
sql: `SELECT c.* FROM computers c WHERE c.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [computer_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import parseJobJson from "@/src/bots-queue/parse-job-json";
|
||||
import type { ComputerWithStatus } from "@/src/types";
|
||||
import computersSql from "@/src/utils/crew-access/computers-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Every computer of this owner, main first.
|
||||
* Every computer this user sees (`computersSql`), main first.
|
||||
*/
|
||||
user_id?: number | string;
|
||||
/**
|
||||
@@ -22,14 +24,18 @@ export default async function grabUserComputers({
|
||||
}: Params): Promise<ComputerWithStatus[]> {
|
||||
if (!user_id && !computer_id) return [];
|
||||
|
||||
const access = computer_id
|
||||
? { sql: `c.id = ?`, values: [Number(computer_id)] }
|
||||
: computersSql({ role: await grabCrewRole({ user_id: user_id! }), user_id: user_id! });
|
||||
|
||||
const rows =
|
||||
(
|
||||
await BunSQLite.sql<ComputerWithStatus & { metadata?: string | null }>({
|
||||
sql: `SELECT c.*, co.status AS status, co.metadata AS metadata FROM computers c
|
||||
LEFT JOIN containers co ON co.id = c.container_id
|
||||
WHERE ${computer_id ? `c.id = ?` : `c.user_id = ?`}
|
||||
WHERE ${access.sql}
|
||||
ORDER BY c.kind = 'main' DESC, c.id ASC`,
|
||||
values: [computer_id || user_id!],
|
||||
values: access.values,
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
|
||||
import grabComputerAudience from "../access/grab-computer-audience";
|
||||
import grabUserComputers from "./grab-user-computers";
|
||||
|
||||
type Params = {
|
||||
@@ -6,16 +7,15 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes a computer's current state (`server:computer-updated`) to its
|
||||
* owner's open tabs. Best effort.
|
||||
* Pushes a computer's current state (`server:computer-updated`) to the
|
||||
* open tabs of everyone who sees it (`grabComputerAudience`). Best effort.
|
||||
*/
|
||||
export default async function pushComputerUpdate({ computer_id }: Params) {
|
||||
const computer = (await grabUserComputers({ computer_id }))[0];
|
||||
|
||||
if (!computer?.user_id) return;
|
||||
if (!computer?.id) return;
|
||||
|
||||
sendToUser({
|
||||
user_id: computer.user_id,
|
||||
data: { event: "server:computer-updated", data: computer },
|
||||
});
|
||||
for (const user_id of await grabComputerAudience({ computer_id: computer.id })) {
|
||||
sendToUser({ user_id, data: { event: "server:computer-updated", data: computer } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppError from "@/src/utils/app-error";
|
||||
import isLockedComputerAccess from "@/src/utils/is-locked-computer-access";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserComputer from "./grab-user-computer";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -19,6 +20,8 @@ export default async function removeBotFromComputer({
|
||||
bot_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const computer = await grabUserComputer({ computer_id, user_id });
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function setBotComputer({ bot_id, user_id, computer_id }: P
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, user_id });
|
||||
const computers = await grabBotComputers({ bot_id: bot.id });
|
||||
const computer = computers.find((c) => String(c.id) == String(computer_id));
|
||||
|
||||
if (!computer?.id) {
|
||||
|
||||
@@ -49,8 +49,8 @@ export default async function createCronJob({
|
||||
const count = Number(
|
||||
(
|
||||
await BunSQLite.sql<{ n: number }>({
|
||||
sql: `SELECT COUNT(*) AS n FROM cron_jobs WHERE bot_id = ?`,
|
||||
values: [bot_id_num],
|
||||
sql: `SELECT COUNT(*) AS n FROM cron_jobs WHERE bot_id = ? AND user_id = ?`,
|
||||
values: [bot_id_num, user_id],
|
||||
})
|
||||
).payload?.[0]?.n || 0,
|
||||
);
|
||||
@@ -64,7 +64,6 @@ export default async function createCronJob({
|
||||
if (computer_id) {
|
||||
const computers = await grabBotComputers({
|
||||
bot_id: bot_id_num,
|
||||
user_id,
|
||||
workspace_id: conversation.workspace_id,
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export default async function fireCronJob({ cron_job, now = Date.now() }: Params
|
||||
}
|
||||
|
||||
const session = await ensureBotSession({
|
||||
user_id: cron_job.user_id!,
|
||||
bot_id: cron_job.bot_id!,
|
||||
workspace_id: cron_job.workspace_id || null,
|
||||
});
|
||||
@@ -57,7 +58,6 @@ export default async function fireCronJob({ cron_job, now = Date.now() }: Params
|
||||
(
|
||||
await grabBotComputers({
|
||||
bot_id: cron_job.bot_id!,
|
||||
user_id: cron_job.user_id!,
|
||||
workspace_id: cron_job.workspace_id || null,
|
||||
})
|
||||
)[0]?.id;
|
||||
|
||||
@@ -8,25 +8,25 @@ import type { EnvsOverviewPayload } from "@/src/types";
|
||||
import grabAllEnvs from "./grab-all-envs";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The super admin looking: workspaces and computers belong to the
|
||||
* install, so they see every one.
|
||||
*/
|
||||
user_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Environment settings page: where envs can be set (general, the
|
||||
* user's workspaces and computers), every env (secret values left out),
|
||||
* The Environment settings page: where envs can be set (general, every
|
||||
* workspace and computer), every env (secret values left out),
|
||||
* and the app settings with where each comes from.
|
||||
*/
|
||||
export default async function grabEnvsOverview({
|
||||
user_id,
|
||||
}: Params): Promise<EnvsOverviewPayload> {
|
||||
export default async function grabEnvsOverview(_params: Params): Promise<EnvsOverviewPayload> {
|
||||
const [workspaces, computers, envs] = await Promise.all([
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT id, name, archived FROM workspaces WHERE user_id = ? ORDER BY archived ASC, id ASC`,
|
||||
values: [user_id],
|
||||
sql: `SELECT id, name, archived FROM workspaces ORDER BY archived ASC, id ASC`,
|
||||
}),
|
||||
BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT id, name, kind FROM computers WHERE user_id = ? ORDER BY kind = 'main' DESC, id ASC`,
|
||||
values: [user_id],
|
||||
sql: `SELECT id, name, kind FROM computers ORDER BY kind = 'main' DESC, id ASC`,
|
||||
}),
|
||||
grabAllEnvs(),
|
||||
]);
|
||||
|
||||
@@ -156,14 +156,14 @@ async function resolveScope({
|
||||
const owned = id
|
||||
? (
|
||||
await BunSQLite.sql({
|
||||
sql: `SELECT id FROM ${table} WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [id, user_id],
|
||||
sql: `SELECT id FROM ${table} WHERE id = ? LIMIT 1`,
|
||||
values: [id],
|
||||
})
|
||||
).payload?.[0]
|
||||
: undefined;
|
||||
|
||||
if (!owned) {
|
||||
throw new AppError(`Pick one of your ${table}.`);
|
||||
throw new AppError(`Pick one of the ${table}.`);
|
||||
}
|
||||
|
||||
return paradigm == "workspace"
|
||||
|
||||
@@ -7,6 +7,7 @@ import grabUserWorkspace from "@/src/functions/backend/db/workspaces/grab-user-w
|
||||
import type { ProfileImageTarget } from "@/src/schemas/profile-image-schema";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import bufferFromBase64 from "@/src/utils/buffer-from-base-64";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -28,6 +29,8 @@ const Targets = {
|
||||
* a private image, the old one deleted. Returns the new `image_media_id`.
|
||||
*/
|
||||
export default async function setProfileImage({ user_id, target, id, data_url }: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const record =
|
||||
target == "bot"
|
||||
? await grabUserBot({ bot_id: id, user_id })
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_CONTAINERS } from "@/db/types/db";
|
||||
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import removeDockerContainer from "@/src/functions/backend/docker/remove-docker-container";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
@@ -21,11 +19,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a user with everything that's theirs (bots, workspaces,
|
||||
* conversations, computers, user types, password, media and its files):
|
||||
* their computers' containers are removed first, their home directories
|
||||
* stay on disk. Their audit log entries stay too. The super admin can't be
|
||||
* deleted, and nobody deletes themselves.
|
||||
* Deletes a user with everything that's theirs (their conversations,
|
||||
* routines, access, what bots remember about them, user types, password,
|
||||
* media and its files). Bots, workspaces and computers belong to the
|
||||
* install and stay, pictures they uploaded for them too. Their audit log
|
||||
* entries stay as well. The super admin can't be deleted, and nobody
|
||||
* deletes themselves.
|
||||
*/
|
||||
export default async function deleteUser({ actor_user_id, actor_user_types, user_id }: Params) {
|
||||
if (String(actor_user_id) == String(user_id)) {
|
||||
@@ -48,24 +47,12 @@ export default async function deleteUser({ actor_user_id, actor_user_types, user
|
||||
throw new AppError(`You can't delete ${user.first_name || "this user"}.`);
|
||||
}
|
||||
|
||||
const containers =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_CONTAINERS>({
|
||||
sql: `SELECT co.* FROM containers co JOIN computers c ON c.container_id = co.id WHERE c.user_id = ?`,
|
||||
values: [user.id],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
for (const container of containers) {
|
||||
if (container.docker_id || container.name) {
|
||||
await removeDockerContainer({ container: container.docker_id || container.name! });
|
||||
}
|
||||
}
|
||||
|
||||
const media_ids =
|
||||
(
|
||||
await BunSQLite.sql<{ id: number }>({
|
||||
sql: `SELECT id FROM media WHERE user_id = ?`,
|
||||
sql: `SELECT id FROM media WHERE user_id = ?
|
||||
AND id NOT IN (SELECT image_media_id FROM bots WHERE image_media_id IS NOT NULL)
|
||||
AND id NOT IN (SELECT image_media_id FROM workspaces WHERE image_media_id IS NOT NULL)`,
|
||||
values: [user.id],
|
||||
})
|
||||
).payload?.map((media) => media.id) || [];
|
||||
@@ -87,12 +74,5 @@ export default async function deleteUser({ actor_user_id, actor_user_types, user
|
||||
await BunSQLite.sql({ sql: `DELETE FROM ${table} WHERE user_id = ?`, values: [user.id] });
|
||||
}
|
||||
|
||||
if (containers.length) {
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM containers WHERE id IN (${containers.map(() => "?").join(", ")})`,
|
||||
values: containers.map((container) => container.id!),
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ export default async function grabUsers(
|
||||
sql += `SELECT\n`;
|
||||
sql += ` u.*,\n`;
|
||||
sql += ` ut_agg.user_types,\n`;
|
||||
sql += ` (SELECT GROUP_CONCAT(bot_id) FROM bot_users WHERE user_id = u.id) AS bot_ids,\n`;
|
||||
sql += ` (SELECT GROUP_CONCAT(workspace_id) FROM workspace_users WHERE user_id = u.id) AS workspace_ids,\n`;
|
||||
/**
|
||||
* Their profile photo only: other primary media (a bot's picture)
|
||||
* must not stand in for it.
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -18,6 +19,8 @@ export default async function addBotToWorkspace({
|
||||
workspace_id,
|
||||
bot_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserComputer from "../computers/grab-user-computer";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -19,6 +20,8 @@ export default async function addComputerToWorkspace({
|
||||
workspace_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const computer = await grabUserComputer({ computer_id, user_id });
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
|
||||
@@ -4,8 +4,13 @@ import type {
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who creates it (a super admin or admin). Workspaces belong to the
|
||||
* install.
|
||||
*/
|
||||
user_id: number | string;
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -13,7 +18,8 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a workspace owned by the user and returns its id.
|
||||
* Creates a workspace and returns its id. Members reach it once they're
|
||||
* added to it (`workspace_users`).
|
||||
*/
|
||||
export default async function createWorkspace({
|
||||
user_id,
|
||||
@@ -21,6 +27,8 @@ export default async function createWorkspace({
|
||||
description,
|
||||
system_prompt,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
assertPromptLength({ label: "System prompt", text: system_prompt });
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
|
||||
@@ -2,6 +2,7 @@ import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -13,6 +14,8 @@ type Params = {
|
||||
* links and its models. Its bots and computers stay.
|
||||
*/
|
||||
export default async function deleteWorkspace({ user_id, workspace_id }: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
if (!workspace?.id) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import workspacesSql from "@/src/utils/crew-access/workspaces-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
workspace_id: number | string;
|
||||
@@ -7,15 +9,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A workspace only if it belongs to the given owner.
|
||||
* A workspace only if the user works in it (`workspacesSql`).
|
||||
*/
|
||||
export default async function grabUserWorkspace({
|
||||
workspace_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_WORKSPACES | null> {
|
||||
const access = workspacesSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT * FROM workspaces WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [workspace_id, user_id],
|
||||
sql: `SELECT w.* FROM workspaces w WHERE w.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [workspace_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { WorkspaceWithActivity } from "@/src/types";
|
||||
import workspacesSql from "@/src/utils/crew-access/workspaces-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The user's workspaces for the rail, not archived, oldest first, each
|
||||
* unread while any conversation in it is.
|
||||
* The workspaces the user works in (`workspacesSql`), for the rail: not
|
||||
* archived, oldest first, each unread while any of the user's
|
||||
* conversations in it is.
|
||||
*/
|
||||
export default async function grabUserWorkspaces({
|
||||
user_id,
|
||||
}: Params): Promise<WorkspaceWithActivity[]> {
|
||||
const access = workspacesSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<WorkspaceWithActivity>({
|
||||
sql: `SELECT w.*,
|
||||
COALESCE((SELECT MAX(s.unread) FROM bot_sessions s WHERE s.workspace_id = w.id), 0) AS unread,
|
||||
(SELECT MAX(s.last_activity_at) FROM bot_sessions s WHERE s.workspace_id = w.id) AS last_activity_at
|
||||
COALESCE((SELECT MAX(s.unread) FROM bot_sessions s WHERE s.workspace_id = w.id AND s.user_id = ?), 0) AS unread,
|
||||
(SELECT MAX(s.last_activity_at) FROM bot_sessions s WHERE s.workspace_id = w.id AND s.user_id = ?) AS last_activity_at
|
||||
FROM workspaces w
|
||||
WHERE w.user_id = ? AND w.archived = 0
|
||||
WHERE w.archived = 0 AND ${access.sql}
|
||||
ORDER BY w.id ASC`,
|
||||
values: [user_id],
|
||||
values: [user_id, user_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
|
||||
@@ -4,6 +4,9 @@ import type {
|
||||
BUN_SQLITE_COBBLE_WORKSPACES,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import workspaceBotsSql from "@/src/utils/crew-access/workspace-bots-sql";
|
||||
import workspacesSql from "@/src/utils/crew-access/workspaces-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
workspace_id: number | string;
|
||||
@@ -22,29 +25,37 @@ export type WorkspaceBotMembership = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One of the user's bots in one of their (non-archived) workspaces: a
|
||||
* member, or the coordinator, which is in all of them. Null otherwise.
|
||||
* A bot the user chats with in a workspace they're in (not archived): one
|
||||
* of its workers, or for the super admin the coordinator, which is in all
|
||||
* of them (`workspaceBotsSql`). Null otherwise.
|
||||
*/
|
||||
export default async function grabWorkspaceBot({
|
||||
workspace_id,
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<WorkspaceBotMembership | null> {
|
||||
const role = await grabCrewRole({ user_id });
|
||||
const workspace_access = workspacesSql({ role, user_id });
|
||||
|
||||
const workspace = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT * FROM workspaces WHERE id = ? AND user_id = ? AND archived = 0 LIMIT 1`,
|
||||
values: [workspace_id, user_id],
|
||||
sql: `SELECT w.* FROM workspaces w WHERE w.id = ? AND w.archived = 0 AND ${workspace_access.sql} LIMIT 1`,
|
||||
values: [workspace_id, ...workspace_access.values],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!workspace?.id) return null;
|
||||
|
||||
const bot_access = workspaceBotsSql({ role, workspace_id: workspace.id });
|
||||
|
||||
const bot = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [bot_id, user_id],
|
||||
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND ${bot_access.sql} LIMIT 1`,
|
||||
values: [bot_id, ...bot_access.values],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!workspace?.id || !bot?.id) return null;
|
||||
if (!bot?.id) return null;
|
||||
|
||||
const membership =
|
||||
(
|
||||
@@ -54,7 +65,5 @@ export default async function grabWorkspaceBot({
|
||||
})
|
||||
).payload?.[0] || null;
|
||||
|
||||
if (!membership && bot.kind != "coordinator") return null;
|
||||
|
||||
return { workspace, bot, membership };
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default async function grabWorkspaceComputers({
|
||||
const res = await BunSQLite.sql<ComputerWithStatus & { metadata?: string | null }>({
|
||||
sql: `SELECT c.*, co.status AS status, co.metadata AS metadata FROM computers c
|
||||
JOIN workspace_computers wc ON wc.computer_id = c.id AND wc.workspace_id = ?
|
||||
JOIN workspaces w ON w.id = wc.workspace_id AND w.user_id = c.user_id
|
||||
JOIN workspaces w ON w.id = wc.workspace_id
|
||||
LEFT JOIN containers co ON co.id = c.container_id
|
||||
ORDER BY c.id = w.computer_id DESC, wc.id ASC`,
|
||||
values: [workspace_id],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -16,6 +17,8 @@ export default async function removeBotFromWorkspace({
|
||||
workspace_id,
|
||||
bot_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
if (!workspace?.id) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -18,6 +19,8 @@ export default async function removeComputerFromWorkspace({
|
||||
workspace_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
if (!workspace?.id) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import AppError from "@/src/utils/app-error";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import _ from "lodash";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -29,6 +30,8 @@ export default async function updateWorkspace({
|
||||
workspace_id,
|
||||
data,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
assertPromptLength({ label: "System prompt", text: data.system_prompt });
|
||||
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
@@ -4,6 +4,7 @@ import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import userAuth from "../../backend/auth/user-auth";
|
||||
import grabSharedPictureMedia from "@/src/functions/backend/db/access/grab-shared-picture-media";
|
||||
|
||||
const { DATA_DIR } = grabDirNames();
|
||||
|
||||
@@ -48,7 +49,10 @@ export default async function handleMediaServer({
|
||||
targetId: media_id,
|
||||
});
|
||||
|
||||
media = media_res.singleRes || undefined;
|
||||
media =
|
||||
media_res.singleRes ||
|
||||
(media_id ? await grabSharedPictureMedia({ media_id, user_id: user.id! }) : null) ||
|
||||
undefined;
|
||||
} else {
|
||||
const media_res = await BunSQLite.select<
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
@@ -62,7 +66,8 @@ export default async function handleMediaServer({
|
||||
|
||||
/**
|
||||
* A private file is only served through `/media/private`, to its
|
||||
* owner.
|
||||
* owner (or, for a bot's or workspace's picture, to whoever sees
|
||||
* it).
|
||||
*/
|
||||
if (media?.is_private) {
|
||||
media = undefined;
|
||||
|
||||
Reference in New Issue
Block a user