224 lines
6.7 KiB
Rust
224 lines
6.7 KiB
Rust
use clap::Parser;
|
|
use std::collections::HashMap;
|
|
use std::error::Error;
|
|
#[macro_use]
|
|
extern crate rocket;
|
|
use reqwest::{Client, StatusCode};
|
|
use rocket::response::content::RawHtml;
|
|
use rocket::serde::json::Json;
|
|
|
|
mod cohost_account;
|
|
mod cohost_posts;
|
|
mod syndication;
|
|
mod webfinger;
|
|
use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL};
|
|
use cohost_posts::{cohost_posts_api_url, CohostPostsPage};
|
|
use webfinger::CohostWebfingerResource;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Args {
|
|
/// The base URL for the corobel instance
|
|
#[clap(short, long, required = true)]
|
|
domain: String,
|
|
/// The base URL for the corobel instance
|
|
#[clap(short, long, default_value_t = default_base_url() )]
|
|
base_url: String,
|
|
}
|
|
|
|
fn default_base_url() -> String {
|
|
"/".into()
|
|
}
|
|
|
|
fn user_agent() -> String {
|
|
format!(
|
|
"{}/{} (RSS feed converter) on {}",
|
|
env!("CARGO_PKG_NAME"),
|
|
env!("CARGO_PKG_VERSION"),
|
|
&ARGS.domain
|
|
)
|
|
}
|
|
|
|
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| Args::parse());
|
|
|
|
#[get("/")]
|
|
fn index() -> RawHtml<&'static str> {
|
|
RawHtml(include_str!("../static/index.html"))
|
|
}
|
|
|
|
#[derive(Responder)]
|
|
#[response(content_type = "application/rss+xml")]
|
|
struct RssResponse {
|
|
inner: String,
|
|
}
|
|
|
|
#[derive(Responder)]
|
|
#[response(content_type = "text/plain")]
|
|
enum ErrorResponse {
|
|
#[response(status = 404)]
|
|
NotFound(String),
|
|
#[response(status = 500)]
|
|
InternalError(String),
|
|
}
|
|
|
|
async fn get_full_post_data(
|
|
client: &mut Client,
|
|
project_id: &str,
|
|
) -> Result<CohostPostsPage, ErrorResponse> {
|
|
let mut page = 0;
|
|
let mut merged_page = get_page_data(client, project_id, page).await?;
|
|
loop {
|
|
let mut new_page = get_page_data(client, project_id, page).await?;
|
|
if new_page.items.is_empty() {
|
|
// Once there are no posts, we're done.
|
|
break;
|
|
} else {
|
|
page += 1;
|
|
merged_page.number_items += new_page.number_items;
|
|
merged_page.items.append(&mut new_page.items);
|
|
}
|
|
}
|
|
Ok(merged_page)
|
|
}
|
|
|
|
async fn get_page_data(
|
|
client: &mut Client,
|
|
project_id: &str,
|
|
page: u64,
|
|
) -> Result<CohostPostsPage, ErrorResponse> {
|
|
let posts_url = cohost_posts_api_url(project_id, page);
|
|
eprintln!("making request to {}", posts_url);
|
|
match client.get(posts_url).send().await {
|
|
Ok(v) => match v.status() {
|
|
StatusCode::OK => match v.json::<CohostPostsPage>().await {
|
|
Ok(page_data) => Ok(page_data),
|
|
Err(e) => {
|
|
let err = format!(
|
|
"Couldn't deserialize Cohost posts page for '{}': {:?}",
|
|
project_id, e
|
|
);
|
|
eprintln!("{}", err);
|
|
return Err(ErrorResponse::InternalError(err));
|
|
}
|
|
},
|
|
// TODO NORA: Handle possible redirects
|
|
s => {
|
|
let err = format!("Didn't receive status code 200 for posts for Cohost project '{}'; got {:?} instead.", page, s);
|
|
eprintln!("{}", err);
|
|
return Err(ErrorResponse::NotFound(err));
|
|
}
|
|
},
|
|
Err(e) => {
|
|
let err = format!(
|
|
"Error making request to Cohost for posts for project '{}': {:?}",
|
|
project_id, e
|
|
);
|
|
eprintln!("{}", err);
|
|
return Err(ErrorResponse::InternalError(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[get("/<project>/feed.rss")]
|
|
async fn syndication_rss_route(project: &str) -> Result<RssResponse, ErrorResponse> {
|
|
let mut client = get_client()?;
|
|
|
|
let project_data = get_project_data(&mut client, project).await?;
|
|
let page_data = get_full_post_data(&mut client, project).await?;
|
|
Ok(RssResponse {
|
|
inner: syndication::channel_for_posts_page(project, project_data, page_data).to_string(),
|
|
})
|
|
}
|
|
|
|
async fn get_project_data(
|
|
client: &mut Client,
|
|
project_id: &str,
|
|
) -> Result<CohostAccount, ErrorResponse> {
|
|
let project_url = format!("{}{}", COHOST_ACCOUNT_API_URL, project_id);
|
|
eprintln!("making request to {}", project_url);
|
|
match client.get(project_url).send().await {
|
|
Ok(v) => match v.status() {
|
|
StatusCode::OK => match v.json::<CohostAccount>().await {
|
|
Ok(a) => Ok(a),
|
|
Err(e) => {
|
|
let err = format!(
|
|
"Couldn't deserialize Cohost project '{}': {:?}",
|
|
project_id, e
|
|
);
|
|
eprintln!("{}", err);
|
|
Err(ErrorResponse::InternalError(err))
|
|
}
|
|
},
|
|
// TODO NORA: Handle possible redirects
|
|
s => {
|
|
let err = format!(
|
|
"Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.",
|
|
project_id, s
|
|
);
|
|
eprintln!("{}", err);
|
|
Err(ErrorResponse::NotFound(err))
|
|
}
|
|
},
|
|
Err(e) => {
|
|
let err = format!(
|
|
"Error making request to Cohost for project '{}': {:?}",
|
|
project_id, e
|
|
);
|
|
eprintln!("{}", err);
|
|
Err(ErrorResponse::InternalError(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_client() -> Result<Client, ErrorResponse> {
|
|
match Client::builder().user_agent(user_agent()).build() {
|
|
Ok(v) => Ok(v),
|
|
Err(e) => {
|
|
let err = format!("Couldn't build a reqwest client: {:?}", e);
|
|
eprintln!("{}", err);
|
|
Err(ErrorResponse::InternalError(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[get("/.well-known/webfinger?<params..>")]
|
|
async fn webfinger_route(
|
|
params: HashMap<String, String>,
|
|
) -> Result<Json<CohostWebfingerResource>, ErrorResponse> {
|
|
if params.len() != 1 {
|
|
let err = format!(
|
|
"Too may or too few parameters. Expected 1, got {}",
|
|
params.len()
|
|
);
|
|
eprintln!("{}", err);
|
|
return Err(ErrorResponse::InternalError(err));
|
|
}
|
|
let mut client = get_client()?;
|
|
if let Some(param) = params.iter().next() {
|
|
let _project_data = get_project_data(&mut client, param.0.as_str()).await?;
|
|
Ok(Json(CohostWebfingerResource::new(
|
|
param.0.as_str(),
|
|
&ARGS.domain,
|
|
&ARGS.base_url,
|
|
)))
|
|
} else {
|
|
Err(ErrorResponse::NotFound("No project ID provided.".into()))
|
|
}
|
|
}
|
|
|
|
#[rocket::main]
|
|
async fn main() -> Result<(), Box<dyn Error>> {
|
|
// Set up the global config
|
|
once_cell::sync::Lazy::force(&ARGS);
|
|
let _rocket = rocket::build()
|
|
.mount(
|
|
&ARGS.base_url,
|
|
routes![index, webfinger_route, syndication_rss_route],
|
|
)
|
|
.ignite()
|
|
.await?
|
|
.launch()
|
|
.await?;
|
|
Ok(())
|
|
}
|