api: auto-detect Greader API path for Miniflux and FreshRSS

Miniflux serves the Greader API at the server root while FreshRSS uses
/api/greader.php. Instead of hardcoding the FreshRSS suffix, try the
URL as-is first (works for Miniflux) and fall back to appending
/api/greader.php (works for FreshRSS). The user just enters the server
URL without needing to know the API path.
This commit is contained in:
Jeena 2026-03-21 11:57:19 +00:00
parent 82aabc080a
commit b49cc69c49
2 changed files with 50 additions and 42 deletions

View file

@ -1,7 +1,9 @@
# FeedTheMonkey # FeedTheMonkey
FeedTheMonkey is a desktop client for any server that implements the FeedTheMonkey is a desktop client for any server that implements the
[Greader API](https://github.com/theoldreader/api) (such as FreshRSS or Miniflux). It doesn't work as a standalone feed reader — [Greader API](https://github.com/theoldreader/api), such as
[FreshRSS](https://freshrss.org) or [Miniflux](https://miniflux.app).
Just enter your server URL — the app detects the API path automatically. It doesn't work as a standalone feed reader —
it connects to a server to fetch articles and sync read state. it connects to a server to fetch articles and sync read state.
This is version 3, rewritten in Rust with GTK4 and libadwaita. This is version 3, rewritten in Rust with GTK4 and libadwaita.

View file

@ -42,14 +42,18 @@ struct Summary {
use crate::model::Article; use crate::model::Article;
fn normalize_base_url(server_url: &str) -> String { /// Return the candidate base URLs to try in order.
/// Miniflux serves the Greader API at the server root;
/// FreshRSS serves it at /api/greader.php.
fn candidate_base_urls(server_url: &str) -> Vec<String> {
let base = server_url.trim_end_matches('/'); let base = server_url.trim_end_matches('/');
// If the user entered just a host (or host/path) without the FreshRSS
// API suffix, append it automatically.
if base.ends_with("/api/greader.php") { if base.ends_with("/api/greader.php") {
base.to_string() vec![base.to_string()]
} else { } else {
format!("{base}/api/greader.php") vec![
base.to_string(),
format!("{base}/api/greader.php"),
]
} }
} }
@ -59,47 +63,49 @@ impl Api {
username: &str, username: &str,
password: &str, password: &str,
) -> Result<Self, String> { ) -> Result<Self, String> {
let base = normalize_base_url(server_url);
let client = Client::new(); let client = Client::new();
let url = format!("{base}/accounts/ClientLogin"); let candidates = candidate_base_urls(server_url);
let resp = client let mut last_err = String::new();
.post(&url)
.form(&[("Email", username), ("Passwd", password)])
.send()
.await
.map_err(|e| e.to_string())?;
let status = resp.status(); for base in candidates {
let body = resp.text().await.map_err(|e| e.to_string())?; let url = format!("{base}/accounts/ClientLogin");
let resp = match client
.post(&url)
.form(&[("Email", username), ("Passwd", password)])
.send()
.await
{
Ok(r) => r,
Err(e) => { last_err = e.to_string(); continue; }
};
if !status.is_success() { let status = resp.status();
let msg = human_error(&body, status.as_u16()); let body = resp.text().await.map_err(|e| e.to_string())?;
return Err(format!("Login failed ({}): {}", status.as_u16(), msg));
if !status.is_success() {
last_err = format!("Login failed ({}): {}", status.as_u16(), human_error(&body, status.as_u16()));
continue;
}
let auth_token = match body.lines().find_map(|l| l.strip_prefix("Auth=")) {
Some(t) => t.to_string(),
None => {
last_err = if looks_like_html(&body) {
format!(
"The server at {base} does not appear to be a \
Greader API endpoint. Check your server URL."
)
} else {
format!("Unexpected response from server: {}", body.trim())
};
continue;
}
};
return Ok(Self { client, server_url: base, auth_token });
} }
let auth_token = body Err(last_err)
.lines()
.find_map(|l| l.strip_prefix("Auth="))
.ok_or_else(|| {
// The server returned 200 but not the expected API response —
// most likely the URL points to a web page, not a FreshRSS API.
if looks_like_html(&body) {
format!(
"The server at {} does not appear to be a FreshRSS \
Google Reader API endpoint. Check your server URL.",
base
)
} else {
format!("Unexpected response from server: {}", body.trim())
}
})?
.to_string();
Ok(Self {
client,
server_url: base,
auth_token,
})
} }
pub async fn fetch_write_token(&self) -> Result<String, String> { pub async fn fetch_write_token(&self) -> Result<String, String> {