Check one feed is in the Directory without building the Directory (#160)

A feed's Directory page and subscribing from it checked the feed may be listed by building the whole
Directory, every feed's listing, every file row for Podcasts and Blogs, every subscription, and
looking for the id in it: 0.57 s for a feed's page in production, nearly all of it that.

The rule (not an OPML or creator itself, neither the feed nor its group private) is now listable(),
which the Directory filters by and the two ask of the catalogue directly. One rule, so the
Directory and the check cannot drift apart; a test covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 01:51:36 +00:00
parent dc340dd4e9
commit e710103ba6
2 changed files with 44 additions and 13 deletions

View File

@@ -954,20 +954,11 @@ async fn popular(state: &WebState, user_id: i64, everything: bool) -> Result<Vec
let counts = db.subscriber_counts().await?;
let media = db.media_feeds().await?;
let catalogue = crate::subscriptions(&state.ctx).await?;
let by_id: std::collections::HashMap<&str, &crate::config::Feed> =
catalogue.iter().map(|s| (s.id.as_str(), &s.cfg)).collect();
let is_folder: std::collections::HashSet<&str> =
catalogue.iter().filter_map(|s| s.cfg.group.as_deref()).collect();
let ok = listable(&catalogue);
let mut out = vec![];
for s in &catalogue {
let n = counts.get(&s.id).copied().unwrap_or(0);
// A feed inside an OPML that looks private is as private as the OPML.
let folder = s.cfg.group.as_deref().and_then(|g| by_id.get(g));
if (n == 0 && !everything)
|| is_folder.contains(s.id.as_str())
|| looks_private(&s.cfg)
|| folder.is_some_and(|f| looks_private(f))
{
if (n == 0 && !everything) || !ok.contains(s.id.as_str()) {
continue;
}
let sum = listed.remove(&s.id).map(|l| l.summary).unwrap_or_default();
@@ -989,6 +980,26 @@ async fn popular(state: &WebState, user_id: i64, everything: bool) -> Result<Vec
Ok(out)
}
/// The feeds the Directory may list: not an OPML or a Patreon creator itself, which is listed as
/// the feeds in it, and neither the feed nor the group it is in private. Asked of one feed, without
/// building the whole Directory, by a feed's Directory page and by subscribing from it: building
/// it was nearly all of the 0.57 s a feed's page took (#160).
fn listable(catalogue: &[crate::Sub]) -> std::collections::HashSet<&str> {
let by_id: std::collections::HashMap<&str, &crate::config::Feed> =
catalogue.iter().map(|s| (s.id.as_str(), &s.cfg)).collect();
let is_folder: std::collections::HashSet<&str> = catalogue.iter().filter_map(|s| s.cfg.group.as_deref()).collect();
catalogue
.iter()
// A feed inside an OPML that looks private is as private as the OPML.
.filter(|s| {
!is_folder.contains(s.id.as_str())
&& !looks_private(&s.cfg)
&& !s.cfg.group.as_deref().and_then(|g| by_id.get(g)).is_some_and(|f| looks_private(f))
})
.map(|s| s.id.as_str())
.collect()
}
async fn get_popular(
State(state): State<WebState>,
user: crate::db::User,
@@ -1047,7 +1058,7 @@ async fn get_listed(
user: crate::db::User,
Path(id): Path<String>,
) -> Result<Json<Listed>, ApiError> {
if !popular(&state, user.id, true).await?.iter().any(|p| p.id == id) {
if !listable(&crate::subscriptions(&state.ctx).await?).contains(id.as_str()) {
return Err(ApiError::not_found(format!("{id:?} is not in the directory")));
}
let order = crate::db::order_sql("published", "desc");
@@ -1082,7 +1093,7 @@ async fn subscribe_popular(
user: crate::db::User,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
if !popular(&state, user.id, true).await?.iter().any(|p| p.id == id) {
if !listable(&crate::subscriptions(&state.ctx).await?).contains(id.as_str()) {
return Err(ApiError::bad_request(format!("{id:?} is not in the directory")));
}
state.ctx.db.subscribe(user.id, &id).await?;
@@ -1248,6 +1259,25 @@ mod tests {
assert!(looks_private(&basic), "a feed with a login configured");
}
#[test]
fn the_directory_lists_feeds_not_their_groups_and_nothing_private() {
let sub = |id: &str, url: &str, group: Option<&str>| crate::Sub {
id: id.into(),
cfg: crate::config::Feed { group: group.map(Into::into), ..feed(url) },
managed: group.is_some(),
};
let catalogue = [
sub("plain", "https://example.com/rss", None),
sub("opml", "https://example.com/list.opml", None),
sub("kid", "https://kid.example/rss", Some("opml")),
sub("paid", "https://www.patreon.com/rss/x?auth=abc", None),
sub("paid-show", "https://www.patreon.com/rss/x?auth=abc&show=1", Some("paid")),
];
let mut got: Vec<&str> = listable(&catalogue).into_iter().collect();
got.sort();
assert_eq!(got, ["kid", "plain"], "the OPML as its feeds; a private feed and its shows never");
}
#[test]
fn only_the_last_admin_is_protected() {
let u = |id, is_admin| crate::db::User {