The Directory comes from the directory service (#205)

ipx asks ipx-directory on its people's behalf, with the token an admin set
in the new Admin, Directory tab, so the page never talks to it and never
sees the token or a feed's address. /api/directory, /api/popular and a
feed's Directory page answer from it; subscribing takes the address from
the directory and goes through Add feed's steps. With no directory set
up, the Directory says so. The local directory code is gone; its privacy
checks are the snapshot builder's now, and their tests moved with them.

With sharing on, ipx sends its snapshot after every scan, whenever the
config changes (set_cfg) and every half hour: each public feed and how
many here subscribe, never an OPML or Patreon creator itself or anything
private. A feed can be marked private in its settings, for a paid feed
whose address does not give it away; anyone subscribed may mark it, only
an admin unmark it. The admin's category for a feed that names none goes
with it, which the directory uses only while the feed names none, so the
category an admin gives a blog still reaches the Directory.

The directory lists only feeds it has read as a feed: the first snapshot
after a start went before the first scan, when an OPML was not yet known
to be one, and the directory listed the OPML. It now fetches what a
snapshot brings in at once instead of within the minute.

The browser suite runs its own ipx-directory on SQLite; the Directory's
tests match feeds by title, since the directory names them by address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 04:31:32 +00:00
parent ed82166745
commit c47aa57909
19 changed files with 710 additions and 246 deletions

View File

@@ -1163,6 +1163,22 @@ const PAID_HOSTS: &[&str] =
#[cfg(test)]
mod tests {
#[test]
fn an_address_carrying_someones_access_looks_private() {
let private = super::url_looks_private;
assert!(!private("https://feeds.twit.tv/twit.xml"));
// Patreon's shape: the key is a query parameter.
assert!(private("https://www.patreon.com/rss/x?auth=abc123&show=2073588"));
assert!(private("https://example.com/rss?api_key=abc"));
// Paid-feed services put the key in the path; the host gives them away.
assert!(private("https://feeds.supercast.com/feeds/abcdefghijklmnopqrstuvwx"));
assert!(private("https://someshow.supportingcast.fm/content/abc123.rss"));
// A long path segment alone is not a key: acast's public show ids look the same.
assert!(!private("https://feeds.acast.com/public/shows/0123456789abcdef01234567"));
assert!(private("https://ray:hunter2@example.com/rss"));
assert!(private("not a url"), "unparseable is not safe to list");
}
/// A server answering by path: /old moves for good to /new, /tmp for now, /chain for good
/// to /tmp, /new is the feed.
async fn redirecting_server() -> String {