Share every picture with everyone signed in

A profile photo, a bot's picture and a workspace's picture all come from
one condition now (sharedPictureMediaSql), so a face is never blank just
because its owner isn't in the viewer's rail. Anything that isn't a
picture stays behind its own check.
This commit is contained in:
2026-10-23 06:01:34 +01:00
parent 93e3a4e3b0
commit b5c3b7d126
7 changed files with 162 additions and 34 deletions
@@ -1,40 +1,31 @@
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";
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
import sharedPictureMediaSql from "./shared-picture-media-sql";
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.
* A picture every signed-in user may see - a member's current profile photo,
* a bot's picture, a workspace's picture (`sharedPictureMediaSql`) - or
* null when this media is something else.
*
* Which crew the viewer reaches plays no part: bots and workspaces belong to
* the install, and people know each other by their photo, so a picture is
* shared with everyone signed in. Everything a picture must not expose (a
* bot's files, a task's deliverable, someone's chat upload) stays behind
* its own check, and a removed picture stops being shared as soon as no
* user, bot or workspace points at it.
*/
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]),
]);
export default async function grabSharedPictureMedia({ media_id }: Params) {
if (!media_id) return null;
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 condition = sharedPictureMediaSql({ alias: "m" });
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_MEDIA>({
sql: `SELECT * FROM media WHERE id = ? LIMIT 1`,
values: [media_id],
sql: `SELECT m.* FROM media m WHERE m.id = ? AND COALESCE(m.archived, 0) <> 1 AND ${condition.sql} LIMIT 1`,
values: [media_id, ...condition.values],
});
return res.payload?.[0] || null;
@@ -0,0 +1,88 @@
import { Database } from "bun:sqlite";
import { beforeAll, describe, expect, test } from "bun:test";
import sharedPictureMediaSql from "./shared-picture-media-sql";
const db = new Database(":memory:");
beforeAll(() => {
db.run(`CREATE TABLE media (id INTEGER PRIMARY KEY, user_id INTEGER, is_primary INTEGER, archived INTEGER)`);
db.run(`CREATE TABLE media_paradigms (media_id INTEGER, media_paradigm TEXT)`);
db.run(`CREATE TABLE bots (id INTEGER PRIMARY KEY, image_media_id INTEGER)`);
db.run(`CREATE TABLE workspaces (id INTEGER PRIMARY KEY, image_media_id INTEGER)`);
db.run(`INSERT INTO media VALUES
(1, 7, 1, 0),
(2, 8, 1, 0),
(3, 8, NULL, 0),
(4, 9, NULL, 0),
(5, 7, NULL, 0),
(6, 7, NULL, 0),
(7, 7, NULL, 0),
(8, 7, NULL, 0)`);
// 1: user 7's current photo, 2: user 8's, 3: an old photo of user 8's (no longer primary)
db.run(`INSERT INTO media_paradigms VALUES
(1, 'user-profile-image'),
(2, 'user-profile-image'),
(3, 'user-profile-image'),
(4, 'bot-image'),
(5, 'bot-image'),
(6, 'workspace-image'),
(7, 'workspace-image'),
(8, 'bot-file')`);
// 4: still bot 2's picture, 5: a bot picture no bot points at any more
db.run(`INSERT INTO bots VALUES (2, 4), (3, NULL)`);
// 6: still workspace 10's picture, 7: a workspace picture nothing points at
db.run(`INSERT INTO workspaces VALUES (10, 6), (11, NULL)`);
});
function visible({ archived }: { archived?: number } = {}) {
const condition = sharedPictureMediaSql();
return db
.query(
`SELECT m.id FROM media m WHERE ${condition.sql} ${
archived === undefined ? `` : `AND COALESCE(m.archived, 0) <> 1`
} ORDER BY m.id`,
)
.all(...condition.values)
.map((row) => (row as { id: number }).id);
}
describe("sharedPictureMediaSql", () => {
test("every user's current photo, and every bot's and workspace's picture", () => {
expect(visible()).toEqual([1, 2, 4, 6]);
});
test("archived media is not a picture anyone gets", () => {
db.run(`UPDATE media SET archived = 1 WHERE id = 2`);
expect(visible({ archived: 0 })).toEqual([1, 4, 6]);
db.run(`UPDATE media SET archived = 0 WHERE id = 2`);
});
test("a photo that is no longer the user's primary is not shared", () => {
expect(visible()).not.toContain(3);
});
test("a picture no bot or workspace points at any more is not shared", () => {
expect(visible()).not.toContain(5);
expect(visible()).not.toContain(7);
});
test("anything else stays private, a bot's file among them", () => {
expect(visible()).not.toContain(8);
});
test("removing a picture row closes it at once", () => {
db.run(`INSERT INTO media VALUES (9, 7, NULL, 0)`);
db.run(`INSERT INTO media_paradigms VALUES (9, 'bot-image')`);
db.run(`UPDATE bots SET image_media_id = 9 WHERE id = 3`);
expect(visible()).toContain(9);
db.run(`UPDATE bots SET image_media_id = NULL WHERE id = 3`);
expect(visible()).not.toContain(9);
});
});
@@ -0,0 +1,41 @@
import type { SqlCondition } from "@/src/utils/crew-access/types";
type Params = {
/**
* The `media` table alias the condition reads.
*/
alias?: string;
};
/**
* The pictures every signed-in user may load, whoever they are and whatever
* crew they reach:
*
* - a member's current profile photo (their primary `user-profile-image`,
* the one `grabUsers` reports as `profile_media_id`),
* - a bot's picture (`bots.image_media_id`),
* - a workspace's picture (`workspaces.image_media_id`).
*
* Bots and workspaces belong to the install, and people know each other by
* their photo, so a picture is never limited to whoever happens to reach
* its owner. It does have to be one still in use: the paradigm alone is not
* enough, since a removed picture leaves its paradigm row behind.
*/
export default function sharedPictureMediaSql({ alias = "m" }: Params = {}): SqlCondition {
return {
sql: `EXISTS (
SELECT 1 FROM media_paradigms mp
WHERE mp.media_id = ${alias}.id
AND (
(mp.media_paradigm = 'user-profile-image' AND ${alias}.is_primary = 1)
OR (mp.media_paradigm = 'bot-image' AND EXISTS (
SELECT 1 FROM bots b WHERE b.image_media_id = ${alias}.id
))
OR (mp.media_paradigm = 'workspace-image' AND EXISTS (
SELECT 1 FROM workspaces w WHERE w.image_media_id = ${alias}.id
))
)
)`,
values: [],
};
}
@@ -35,7 +35,12 @@ export default async function grabBotFileMedia({
const role = await grabCrewRole({ user_id });
const reaches =
file.scope == "workspace"
? Boolean(await grabUserWorkspace({ workspace_id: file.owner_id!, user_id }))
? Boolean(
await grabUserWorkspace({
workspace_id: file.owner_id!,
user_id,
}),
)
: role == "super_admin"
? true
: Boolean(await grabUserBot({ bot_id: file.owner_id!, user_id }));
@@ -15,8 +15,8 @@ type Params = {
* gate in front of the file on disk:
*
* - the user's own uploads (`media.user_id`),
* - a bot's or workspace's picture, to whoever reaches that bot or
* workspace (`grabSharedPictureMedia`),
* - a picture every signed-in user may see: a member's current profile
* photo, a bot's picture, a workspace's picture (`grabSharedPictureMedia`),
* - a bot's or workspace's file, to whoever reaches the folder it lives in
* (`grabBotFileMedia`),
* - a file attached to a task, to whoever reaches the task
@@ -48,7 +48,7 @@ export default async function grabUserMedia({
return (
own.singleRes ||
(await grabSharedPictureMedia({ media_id, user_id })) ||
(await grabSharedPictureMedia({ media_id })) ||
(await grabBotFileMedia({ media_id, user_id })) ||
(await grabTaskMediaFile({ media_id, user_id })) ||
undefined
@@ -30,7 +30,10 @@ export default async function grabTaskMediaFile({
if (!link?.task_id) return null;
const access = taskScopesSql({ role: await grabCrewRole({ user_id }), user_id });
const access = taskScopesSql({
role: await grabCrewRole({ user_id }),
user_id,
});
const task = (
await BunSQLite.sql<{ id: number }>({
@@ -16,9 +16,9 @@ type Params = {
*
* There is no public media. Every URL is private, so the request has to be
* signed in and the user has to reach the file (`grabUserMedia` decides
* that: their own upload, a bot's or workspace's file or picture they can
* chat with, a file on one of their tasks). Anything else is answered with
* a 404, never with the file.
* that: their own upload, any profile photo, bot picture or workspace
* picture, a bot's or workspace's file they can reach, a file on one of
* their tasks). Anything else is answered with a 404, never with the file.
*/
export default async function handleMediaServer({
req,