Add bots files handling logic

This commit is contained in:
2026-10-21 17:56:09 +01:00
parent b0af4db1bf
commit d17a4a7bdd
97 changed files with 5843 additions and 41 deletions
@@ -1,5 +1,6 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
import purgeStorage, { grabStorageMediaIds } from "@/src/functions/backend/db/bot-files/purge-storage";
import AppError from "@/src/utils/app-error";
import grabUserBot from "./grab-user-bot";
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
@@ -27,6 +28,13 @@ export default async function deleteBot({ user_id, bot_id }: Params) {
throw new AppError(`The coordinator can't be deleted.`);
}
/**
* Read before the delete: the files' rows go with the bot.
*/
const file_media_ids = await grabStorageMediaIds({ scope: "bot", owner_id: bot.id! }).catch(
() => [],
);
const res = await BunSQLite.sql({ sql: `DELETE FROM bots WHERE id = ?`, values: [bot.id] });
if (!res.success) {
@@ -37,5 +45,11 @@ export default async function deleteBot({ user_id, bot_id }: Params) {
await deleteMedia({ id: bot.image_media_id, user_id: Number(user_id) }).catch(() => {});
}
/**
* Its folder goes with it: the bytes, their `media` rows and the search
* vectors.
*/
await purgeStorage({ scope: "bot", owner_id: bot.id!, media_ids: file_media_ids });
return bot;
}
@@ -4,7 +4,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
import grabMessagesMedia from "./grab-messages-media";
import grabMessagesReactions from "./grab-messages-reactions";
import grabMessagesReplies from "./grab-messages-replies";
import mediaToAttachment from "./media-to-attachment";
import mediaToAttachment, { grabAttachmentFileIds } from "./media-to-attachment";
type Params = {
session_id: number | string;
@@ -61,6 +61,14 @@ export default async function grabBotMessages({
: Promise.resolve(new Map()),
]);
/**
* Files that live in a bot's folder carry their `bot_files` id, so a
* chip opens the file rather than only its bytes.
*/
const file_ids = await grabAttachmentFileIds({
media_ids: [...media.values()].flat().map((file) => Number(file.id)),
});
return messages.map((message) => {
const id = Number(message.id);
const files = media.get(id);
@@ -69,7 +77,14 @@ export default async function grabBotMessages({
return {
...message,
...(files
? { attachments: files.map((file) => mediaToAttachment({ media: file })) }
? {
attachments: files.map((file) =>
mediaToAttachment({
media: file,
file_id: file_ids.get(Number(file.id)),
}),
),
}
: {}),
...(reply_to ? { reply_to } : {}),
...(user_id ? { reactions: reactions.get(id) || [] } : {}),
@@ -13,6 +13,11 @@ type Params = {
workspace_id?: number | null;
text: string;
needs_you?: boolean;
/**
* Files the bot saved and sent with it (`media` ids): a PDF it made, a
* generated image, a document it wrote.
*/
media_ids?: number[];
};
/**
@@ -25,6 +30,7 @@ export default async function insertBotMessage({
workspace_id,
text,
needs_you,
media_ids = [],
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES> {
const now = Date.now();
const needs_you_flag = needs_you ? 1 : 0;
@@ -52,6 +58,13 @@ export default async function insertBotMessage({
throw new Error(`Failed to save message: ${res.msg}`);
}
for (const media_id of media_ids) {
await BunSQLite.sql({
sql: `INSERT INTO bot_message_media (created_at, updated_at, message_id, media_id) VALUES (?, ?, ?, ?)`,
values: [now, now, id, media_id],
}).catch(() => {});
}
await BunSQLite.sql({
sql: `UPDATE bot_sessions SET last_activity_at = ?, unread = 1 WHERE id = ?`,
values: [now, session_id],
@@ -1,16 +1,23 @@
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOT_FILES, BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
import type { BotMessageAttachment, MediaType } from "@/src/types";
type Params = {
media: BUN_SQLITE_COBBLE_MEDIA;
/**
* The bot file it is, when the caller already knows (avoids a lookup
* per attachment).
*/
file_id?: number | null;
};
/**
* A `media` row as a chat attachment: its original name (`title`), type
* and the private URLs it's served at.
* and the private URLs it's served at. A file saved in a bot's folder
* carries its `file_id`, so the chip can open the file's page.
*/
export default function mediaToAttachment({ media }: Params): BotMessageAttachment {
export default function mediaToAttachment({ media, file_id }: Params): BotMessageAttachment {
const { media_url, media_thumbnail_url } = grabMediaURL({ media: { ...media } });
return {
@@ -19,5 +26,29 @@ export default function mediaToAttachment({ media }: Params): BotMessageAttachme
media_type: (media.media_type || "document") as MediaType,
url: media_url || "",
thumbnail_url: media_thumbnail_url,
file_id: file_id || undefined,
};
}
/**
* The `bot_files` rows behind a message's media, keyed by media id, so the
* attachments can carry their file ids without a query each.
*/
export async function grabAttachmentFileIds({ media_ids }: { media_ids: number[] }) {
const ids = media_ids.filter((id) => id > 0);
if (!ids.length) return new Map<number, number>();
const placeholders = ids.map(() => "?").join(", ");
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_FILES>({
sql: `SELECT id, media_id FROM bot_files WHERE media_id IN (${placeholders})`,
values: ids,
});
return new Map(
(res.payload || [])
.filter((file) => file.media_id)
.map((file) => [Number(file.media_id), Number(file.id)]),
);
}