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:
parent
82aabc080a
commit
b49cc69c49
2 changed files with 50 additions and 42 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
60
src/api.rs
60
src/api.rs
|
|
@ -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 candidates = candidate_base_urls(server_url);
|
||||||
|
let mut last_err = String::new();
|
||||||
|
|
||||||
|
for base in candidates {
|
||||||
let url = format!("{base}/accounts/ClientLogin");
|
let url = format!("{base}/accounts/ClientLogin");
|
||||||
let resp = client
|
let resp = match client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.form(&[("Email", username), ("Passwd", password)])
|
.form(&[("Email", username), ("Passwd", password)])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => { last_err = e.to_string(); continue; }
|
||||||
|
};
|
||||||
|
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let body = resp.text().await.map_err(|e| e.to_string())?;
|
let body = resp.text().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
let msg = human_error(&body, status.as_u16());
|
last_err = format!("Login failed ({}): {}", status.as_u16(), human_error(&body, status.as_u16()));
|
||||||
return Err(format!("Login failed ({}): {}", status.as_u16(), msg));
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_token = body
|
let auth_token = match body.lines().find_map(|l| l.strip_prefix("Auth=")) {
|
||||||
.lines()
|
Some(t) => t.to_string(),
|
||||||
.find_map(|l| l.strip_prefix("Auth="))
|
None => {
|
||||||
.ok_or_else(|| {
|
last_err = if looks_like_html(&body) {
|
||||||
// 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!(
|
format!(
|
||||||
"The server at {} does not appear to be a FreshRSS \
|
"The server at {base} does not appear to be a \
|
||||||
Google Reader API endpoint. Check your server URL.",
|
Greader API endpoint. Check your server URL."
|
||||||
base
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("Unexpected response from server: {}", body.trim())
|
format!("Unexpected response from server: {}", body.trim())
|
||||||
|
};
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
})?
|
};
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
return Ok(Self { client, server_url: base, auth_token });
|
||||||
client,
|
}
|
||||||
server_url: base,
|
|
||||||
auth_token,
|
Err(last_err)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_write_token(&self) -> Result<String, String> {
|
pub async fn fetch_write_token(&self) -> Result<String, String> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue