Make the interests table the only source of standing interests (step 3)
The OPML file and the profile's ## Interests section become one-time import inputs; the prompt groups by the stored category and the OPML config key is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
This commit is contained in:
+53
-1
@@ -3,7 +3,7 @@
|
||||
//! Interest queries stay here so the central database layer remains focused on
|
||||
//! the pipeline's shared records.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use jiff::Timestamp;
|
||||
@@ -64,6 +64,35 @@ pub struct Rates {
|
||||
|
||||
const INTEREST_COLUMNS: &str = "id, name, category, created_at, categorized_at";
|
||||
|
||||
/// Parse an OPML export for the one-time interests importer.
|
||||
pub fn parse_opml(raw: &str) -> Vec<String> {
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut out = Vec::new();
|
||||
for chunk in raw.split("text=\"").skip(1) {
|
||||
let Some((value, _)) = chunk.split_once('"') else {
|
||||
continue;
|
||||
};
|
||||
let name = xml_unescape(value).trim().to_string();
|
||||
if !name.is_empty() && seen.insert(name.to_lowercase()) {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn xml_unescape(value: &str) -> String {
|
||||
if !value.contains('&') {
|
||||
return value.to_string();
|
||||
}
|
||||
value
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("'", "'")
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
fn interest_from(row: &sqlx::sqlite::SqliteRow) -> Interest {
|
||||
Interest {
|
||||
id: row.get("id"),
|
||||
@@ -326,6 +355,29 @@ mod tests {
|
||||
(dir, db)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opml_parser_unescapes_trims_and_deduplicates_names() {
|
||||
let interests = parse_opml(
|
||||
r#"<opml><body>
|
||||
<outline text=" Rust "/>
|
||||
<outline text="E-Ink & RSS"/>
|
||||
<outline text="rust"/>
|
||||
<outline text="Quotes "and" apostrophes 'x' 'y'"/>
|
||||
<outline text="Markup <tag>"/>
|
||||
<outline text=""/>
|
||||
</body></opml>"#,
|
||||
);
|
||||
assert_eq!(
|
||||
interests,
|
||||
[
|
||||
"Rust",
|
||||
"E-Ink & RSS",
|
||||
"Quotes \"and\" apostrophes 'x' 'y'",
|
||||
"Markup <tag>",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async fn seed_article(db: &Db, id: ArticleId) {
|
||||
sqlx::query(
|
||||
"INSERT INTO articles (id, canonical_url, title, first_seen) VALUES (?, ?, ?, ?)",
|
||||
|
||||
Reference in New Issue
Block a user