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:
56
src/web.rs
56
src/web.rs
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user