v4: a dump or playlist going public notifies its followers — a private dump notified no one when posted, and nothing told its followers when it was made public: the poster's followers never learnt of it, nor did the followers of the public playlists holding it. Likewise, the dumps added to a playlist while it was private were never notified to its followers. updateDump and replaceFileDump now call notifyIfWentPublic, which notifies the poster's followers (user_dump_posted) and, through the new notifyPlaylistFollowersNowPublic, the followers of each public playlist holding the dump (playlist_dump_added). updatePlaylist calls the same function when a playlist goes public, for each public dump in it, from the bottom up. A follower hears of a dump once both it and the playlist are public; the source keys stop a dump made private and public again from notifying twice. 256 backend tests pass, and deno check, deno fmt and doc:lint are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 10:23:08 +00:00
parent 256857f72e
commit 7713c5a1b5
5 changed files with 142 additions and 6 deletions

View File

@@ -34,6 +34,7 @@ import { isValidHttpUrl } from "../remote/fetch.ts";
import { broadcastDumpChange } from "./ws-service.ts";
import {
notifyMentions,
notifyPlaylistFollowersNowPublic,
notifyUserFollowersNewDump,
} from "./notification-service.ts";
import { changeSlug, newSlug, resolveSlug } from "./slug-service.ts";
@@ -91,6 +92,19 @@ function titleFromUrl(url: string): string {
}
}
/**
* Notifies the followers of a dump that went public, `before` being private
* and `after` public: the poster's, as if it were just posted, and those of
* each public playlist holding it, as if it were just added. A private dump's
* followers were never told of it, so an edit that makes it public is when
* they learn of it.
*/
function notifyIfWentPublic(before: Dump, after: Dump): void {
if (!before.isPrivate || after.isPrivate) return;
notifyUserFollowersNewDump(after.userId, after.id, after.title);
notifyPlaylistFollowersNowPublic({ dumpId: after.id });
}
/**
* Posts a link as `userId`, for `POST /api/dumps`, with its preview fetched
* from the page. A public dump is broadcast as `dump_created` and notified to
@@ -427,7 +441,8 @@ export function findDumpsByUrl(
* which `exempt` (a moderator) frees from its rate limit; a new title leaves
* the slug as it is. Clients get the dump as `dump_updated`, or a
* `dump_deleted` when it turns private and a `dump_created` when it turns
* public. Anyone @mentioned in the note is notified.
* public. Anyone @mentioned in the note is notified, and when it turns public,
* its followers are as when it is posted ({@link notifyIfWentPublic}).
*
* @throws {APIException} 404 when there is no such dump, 400 when a new URL
* isn't a public http(s) URL, a category doesn't exist or the slug isn't
@@ -484,6 +499,7 @@ export async function updateDump(
attachCategoryIds([updatedDump]);
}
broadcastDumpChange(dump, updatedDump);
notifyIfWentPublic(dump, updatedDump);
if (updatedDump.comment) {
if (!updatedDump.isPrivate) {
notifyMentions(
@@ -555,6 +571,7 @@ export async function updateDump(
}
broadcastDumpChange(dump, updatedDump);
notifyIfWentPublic(dump, updatedDump);
if (updatedDump.comment) {
if (!updatedDump.isPrivate) {
notifyMentions(
@@ -577,8 +594,8 @@ export async function updateDump(
* `isPrivate` left out stays as it is. Visibility is taken here so that a new
* file and a change of visibility land together. The slug changes as in
* {@link updateDump}, before the file is written. The old file is put back if
* the database update fails, and clients are told as {@link updateDump} tells
* them.
* the database update fails, and clients and followers are told as
* {@link updateDump} tells them.
*
* @throws {APIException} 404 when there is no such dump, 400 when it isn't a
* file dump, the title or comment is too long, the file's type isn't allowed,
@@ -683,6 +700,7 @@ export async function replaceFileDump(
updatedAt: now,
};
broadcastDumpChange(dump, updatedDump);
notifyIfWentPublic(dump, updatedDump);
return updatedDump;
}

View File

@@ -230,7 +230,9 @@ export function notifyPlaylistOwnerNewFollower(
/**
* Notifies everyone following `dumperId` that they posted a dump. The dump
* service calls it for a public dump only, when it is created.
* service calls it for a public dump only, when it is created or goes public.
* A follower is told of a dump once, however often it goes private and public
* again.
*/
export function notifyUserFollowersNewDump(
dumperId: string,
@@ -473,3 +475,46 @@ export function notifyPlaylistFollowersNewDump(
}
});
}
/**
* Notifies the followers of each playlist of the dumps in it that became
* visible to them: a playlist's followers are told of a dump once both are
* public. The dump service calls it with `dumpId` when a dump goes public, for
* each public playlist holding it; the playlist service with `playlistId`
* when a playlist goes public, for each public dump in it, from the bottom of
* the playlist up. A dump the followers were told of before either went
* private isn't notified again. Adding a dump to a public playlist calls
* {@link notifyPlaylistFollowersNewDump} directly.
*/
export function notifyPlaylistFollowersNowPublic(
of: { dumpId: string } | { playlistId: string },
): void {
const [column, id] = "dumpId" in of
? ["pd.dump_id", of.dumpId]
: ["pd.playlist_id", of.playlistId];
const rows = db.prepare(
`SELECT p.id AS playlist_id, p.title AS playlist_title,
d.id AS dump_id, d.title AS dump_title
FROM playlist_dumps pd
JOIN playlists p ON p.id = pd.playlist_id
JOIN dumps d ON d.id = pd.dump_id
WHERE ${column} = ? AND p.is_public = 1 AND d.is_private = 0
ORDER BY pd.position DESC;`,
).all(id) as {
playlist_id: string;
playlist_title: string;
dump_id: string;
dump_title: string;
}[];
transaction(() => {
for (const row of rows) {
notifyPlaylistFollowersNewDump(
row.playlist_id,
row.playlist_title,
row.dump_id,
row.dump_title,
);
}
});
}

View File

@@ -33,6 +33,7 @@ import {
import {
notifyMentions,
notifyPlaylistFollowersNewDump,
notifyPlaylistFollowersNowPublic,
} from "./notification-service.ts";
import { changeSlug, newSlug, resolveSlug } from "./slug-service.ts";
import { linkAttachments } from "./attachment-service.ts";
@@ -207,8 +208,9 @@ export function listPlaylistsByUser(
* moderator is exempt from its rate limit; a new title leaves the slug as it
* is. `canModerate` lets someone other than the owner edit it. If the
* playlist is public, it notifies, in the owner's name, the users the
* description mentions. It links the description's attachments and
* broadcasts `playlist_updated`.
* description mentions. When it goes public, its followers are notified of
* its public dumps they weren't yet told of. It links the description's
* attachments and broadcasts `playlist_updated`.
*
* @throws {APIException} 404 when there is no such playlist, 403 when the user
* is neither its owner nor a moderator, 400 when the slug isn't valid, 409
@@ -270,6 +272,10 @@ export function updatePlaylist(
linkAttachments(newDescription, "playlist", playlist.id);
}
broadcastPlaylistUpdated(updated, playlist.isPublic);
// Its followers weren't told of the dumps added while it was private.
if (!playlist.isPublic && newIsPublic) {
notifyPlaylistFollowersNowPublic({ playlistId: playlist.id });
}
return updated;
}