/** * Decides which of a playlist's dumps a viewer may see. The playlist service * filters its listings with it, the WebSocket service filters each client's * `playlist_dumps_updated` message the same way, and the queries that list * playlists count their dumps by the same rule. * * @module */ import type { Playlist } from "../../shared/playlist.ts"; /** * The fields of a playlist's dump that {@link isMemberVisibleTo} needs. A * `Dump` has them. For its broadcasts, the playlist service reads only these. */ export interface PlaylistMember { id: string; isPrivate: boolean; /** The dump's poster, not the playlist's owner. */ userId: string; } /** * Checks whether `viewerId` may see `member` in `playlist`. A public dump is * visible to all. A private dump is visible only to the playlist's owner, and * only if they posted it: moderators don't see it, and neither does the owner * once someone else's dump in their playlist goes private. A null or undefined * `viewerId` is a signed-out viewer. */ export function isMemberVisibleTo( member: PlaylistMember, playlist: Pick, viewerId: string | null | undefined, ): boolean { return !member.isPrivate || (member.userId === viewerId && viewerId === playlist.userId); } /** * The fields of a playlist's dump that {@link dumpFiguresFor} needs: those * that tell who may see it, and when it was added. The playlist service reads * them for its broadcasts. */ export interface DatedPlaylistMember extends PlaylistMember { addedAt: Date; } /** A figure of the dumps of `playlists p` that the viewer `?1` may see. */ const visibleDumps = (figure: string) => `(SELECT ${figure} FROM playlist_dumps pd INNER JOIN dumps d ON d.id = pd.dump_id WHERE pd.playlist_id = p.id AND (d.is_private = 0 OR (d.user_id = p.user_id AND p.user_id = ?1)) )`; /** * The `dump_count` and `last_dump_at` columns of a query on `playlists p`: * how many of the playlist's dumps the viewer may see, by * {@link isMemberVisibleTo}'s rule, and when the latest of them was added, * null without any. Both read the viewer's id, or `""` for a signed-out one, * as `?1`: it is the query's first parameter, and each `?` after it takes the * next. */ export const VISIBLE_DUMP_FIGURES = `${visibleDumps("COUNT(*)")} as dump_count, ${visibleDumps("MAX(pd.added_at)")} as last_dump_at`; /** * Returns what `playlist` says of its dumps to the viewer, `members` being * all of them: the `dumpCount` and `lastDumpAt` that * {@link VISIBLE_DUMP_FIGURES} reads in a query. The WebSocket service puts * them on the playlist each socket is sent. */ export function dumpFiguresFor( members: DatedPlaylistMember[], playlist: Pick, viewerId: string | null | undefined, ): Required> & Pick { const visible = members.filter((m) => isMemberVisibleTo(m, playlist, viewerId) ); return { dumpCount: visible.length, lastDumpAt: visible.length ? new Date(Math.max(...visible.map((m) => m.addedAt.getTime()))) : undefined, }; }