Trim the state database; Popular lists feeds the way Directory does

Drops the created columns on users, subscriptions and sessions, which were
written by every insert and read by nothing, and migrate()'s add list, whose
columns all predate 0.3.0. Removes Db::subscribed_feed_ids (no callers),
Db::subscriber_count (one caller wanting > 0) and Managed.orphaned (never
read). The old-database test now builds the tables with foreign keys on.

Popular now lists the feeds inside an OPML or a Patreon creator, never the
collection, as Directory does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
This commit is contained in:
2026-09-12 13:15:55 +00:00
parent 457a58dcc5
commit a958f7cb37
10 changed files with 159 additions and 157 deletions

View File

@@ -579,14 +579,10 @@ struct PopularRow {
}
/// Every feed that may be listed, with everyone counted, you included, most subscribers
/// first. Popular is the top of one list and the directory is all of the other, and between
/// them they are all that `subscribe_popular` will subscribe you to.
///
/// `folders` says how an OPML appears. Popular lists the OPML once and not the feeds inside:
/// everyone subscribed to it counts for every one of them, and eighty of those would bury
/// everything anyone chose on purpose. The directory, which is for finding a show, lists the
/// feeds inside and never the OPML.
fn popular(state: &WebState, user_id: i64, folders: bool) -> Result<Vec<PopularRow>> {
/// first. Popular is the top of it, the directory is all of it, and it is all that
/// `subscribe_popular` will subscribe you to. An OPML or a Patreon creator is listed as the
/// feeds inside it and never itself: both lists are for finding a show.
fn popular(state: &WebState, user_id: i64) -> Result<Vec<PopularRow>> {
let db = &state.ctx.db;
let mine: std::collections::HashSet<String> =
db.subscriptions_for(user_id)?.into_iter().map(|s| s.feed_id).collect();
@@ -599,11 +595,13 @@ fn popular(state: &WebState, user_id: i64, folders: bool) -> Result<Vec<PopularR
let mut out = vec![];
for s in &catalogue {
let n = counts.get(&s.id).copied().unwrap_or(0);
let inside = s.managed || s.cfg.group.is_some();
let hidden = if folders { inside } else { is_folder.contains(s.id.as_str()) };
// 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 || hidden || looks_private(&s.cfg) || folder.is_some_and(|f| looks_private(f)) {
if n == 0
|| is_folder.contains(s.id.as_str())
|| looks_private(&s.cfg)
|| folder.is_some_and(|f| looks_private(f))
{
continue;
}
let sum = db.feed_summary(&s.id)?;
@@ -618,7 +616,7 @@ async fn get_popular(
State(state): State<WebState>,
user: crate::db::User,
) -> Result<Json<Vec<PopularRow>>, ApiError> {
let mut rows = popular(&state, user.id, true)?;
let mut rows = popular(&state, user.id)?;
rows.truncate(10);
Ok(Json(rows))
}
@@ -628,7 +626,7 @@ async fn get_directory(
State(state): State<WebState>,
user: crate::db::User,
) -> Result<Json<Vec<PopularRow>>, ApiError> {
let mut rows = popular(&state, user.id, false)?;
let mut rows = popular(&state, user.id)?;
rows.sort_by_key(sort_name);
Ok(Json(rows))
}
@@ -637,15 +635,14 @@ fn sort_name(p: &PopularRow) -> String {
p.title.clone().unwrap_or_else(|| p.id.clone()).to_lowercase()
}
/// Subscribes by id, because the lists never show a URL. Checked against the same lists, so
/// Subscribes by id, because the list never shows a URL. Checked against the same list, so
/// a guessed id cannot reach a private feed.
async fn subscribe_popular(
State(state): State<WebState>,
user: crate::db::User,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
let listed = |folders| popular(&state, user.id, folders).map(|rows| rows.iter().any(|p| p.id == id));
if !(listed(true)? || listed(false)?) {
if !popular(&state, user.id)?.iter().any(|p| p.id == id) {
return Err(ApiError::bad_request(format!("{id:?} is not in the directory")));
}
state.ctx.db.subscribe(user.id, &id)?;
@@ -1095,7 +1092,7 @@ async fn remove_feed(
{
state.ctx.db.unsubscribe(user.id, &child.id)?;
}
if state.ctx.db.subscriber_count(&id)? > 0 {
if state.ctx.db.subscriber_counts()?.contains_key(&id) {
return Ok(StatusCode::NO_CONTENT);
}