corobel/src/main.rs

224 lines
7.1 KiB
Rust
Raw Normal View History

2022-11-01 01:57:20 +00:00
use clap::Parser;
2022-10-30 17:36:09 +00:00
use std::collections::HashMap;
2022-10-30 14:26:34 +00:00
use std::error::Error;
2022-11-01 01:57:20 +00:00
#[macro_use]
extern crate rocket;
2022-11-01 02:53:32 +00:00
use reqwest::{Client, StatusCode};
2022-11-01 01:57:20 +00:00
use rocket::response::content::RawHtml;
2022-10-30 17:36:09 +00:00
use rocket::serde::json::Json;
2022-10-30 14:26:34 +00:00
2022-10-30 17:36:09 +00:00
mod cohost_account;
2022-11-01 01:57:20 +00:00
mod cohost_posts;
mod syndication;
2022-10-30 17:36:09 +00:00
mod webfinger;
use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL};
2022-11-01 01:57:20 +00:00
use cohost_posts::{cohost_posts_api_url, CohostPostsPage};
2022-10-30 17:36:09 +00:00
use webfinger::CohostWebfingerResource;
2022-10-30 14:26:34 +00:00
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
2022-11-01 01:57:20 +00:00
/// The base URL for the corobel instance
2022-10-30 17:36:09 +00:00
#[clap(short, long, required = true)]
domain: String,
2022-11-01 01:57:20 +00:00
/// The base URL for the corobel instance
2022-10-30 17:36:09 +00:00
#[clap(short, long, default_value_t = default_base_url() )]
base_url: String,
2022-10-30 14:26:34 +00:00
}
2022-11-01 01:57:20 +00:00
fn default_base_url() -> String {
"/".into()
}
2022-11-01 02:53:32 +00:00
fn user_agent() -> String {
format!(
"{}/{} (RSS feed converter) on {}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
&ARGS.domain
)
}
2022-11-01 01:57:20 +00:00
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"))
}
2022-11-01 02:21:05 +00:00
#[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),
}
2022-11-01 01:57:20 +00:00
#[get("/<project>/feed.rss?<page>")]
2022-11-01 02:21:05 +00:00
async fn syndication_rss_route(
project: &str,
page: Option<u64>,
) -> Result<RssResponse, ErrorResponse> {
2022-11-01 01:57:20 +00:00
let page = page.unwrap_or(0);
let project_url = format!("{}{}", COHOST_ACCOUNT_API_URL, project);
let posts_url = cohost_posts_api_url(project, page);
2022-10-30 14:26:34 +00:00
2022-11-01 02:53:32 +00:00
let client = match Client::builder().user_agent(user_agent()).build() {
Ok(v) => v,
Err(e) => {
let err = format!("Couldn't build a reqwest client: {:?}", e);
eprintln!("{}", err);
return Err(ErrorResponse::InternalError(err));
}
};
2022-11-01 01:57:20 +00:00
eprintln!("making request to {}", project_url);
2022-11-01 02:53:32 +00:00
let project_data: CohostAccount = match client.get(project_url).send().await {
2022-11-01 01:57:20 +00:00
Ok(v) => match v.status() {
StatusCode::OK => match v.json::<CohostAccount>().await {
Ok(a) => a,
Err(e) => {
2022-11-01 02:21:05 +00:00
let err = format!("Couldn't deserialize Cohost project '{}': {:?}", project, e);
eprintln!("{}", err);
return Err(ErrorResponse::InternalError(err));
2022-11-01 01:57:20 +00:00
}
},
// TODO NORA: Handle possible redirects
s => {
2022-11-01 02:21:05 +00:00
let err = format!(
2022-11-01 01:57:20 +00:00
"Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.",
project, s
);
2022-11-01 02:21:05 +00:00
eprintln!("{}", err);
return Err(ErrorResponse::NotFound(err));
2022-11-01 01:57:20 +00:00
}
},
Err(e) => {
2022-11-01 02:21:05 +00:00
let err = format!(
2022-11-01 01:57:20 +00:00
"Error making request to Cohost for project '{}': {:?}",
project, e
);
2022-11-01 02:21:05 +00:00
eprintln!("{}", err);
return Err(ErrorResponse::InternalError(err));
2022-11-01 01:57:20 +00:00
}
};
2022-10-30 14:26:34 +00:00
2022-11-01 01:57:20 +00:00
eprintln!("making request to {}", posts_url);
2022-11-01 02:53:32 +00:00
match client.get(posts_url).send().await {
2022-11-01 01:57:20 +00:00
Ok(v) => match v.status() {
StatusCode::OK => match v.json::<CohostPostsPage>().await {
Ok(page_data) => {
2022-11-01 02:21:05 +00:00
return Ok(RssResponse {
inner: syndication::channel_for_posts_page(
project,
page,
project_data,
page_data,
)
.to_string(),
});
2022-11-01 01:57:20 +00:00
}
Err(e) => {
2022-11-01 02:21:05 +00:00
let err = format!(
2022-11-01 01:57:20 +00:00
"Couldn't deserialize Cohost posts page for '{}': {:?}",
project, e
);
2022-11-01 02:21:05 +00:00
eprintln!("{}", err);
return Err(ErrorResponse::InternalError(err));
2022-11-01 01:57:20 +00:00
}
},
// TODO NORA: Handle possible redirects
s => {
2022-11-01 02:21:05 +00:00
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));
2022-11-01 01:57:20 +00:00
}
},
Err(e) => {
2022-11-01 02:21:05 +00:00
let err = format!(
2022-11-01 01:57:20 +00:00
"Error making request to Cohost for posts for project '{}': {:?}",
project, e
);
2022-11-01 02:21:05 +00:00
eprintln!("{}", err);
return Err(ErrorResponse::InternalError(err));
2022-11-01 01:57:20 +00:00
}
};
}
#[get("/.well-known/webfinger?<params..>")]
2022-10-30 17:36:09 +00:00
async fn webfinger_route(params: HashMap<String, String>) -> Option<Json<CohostWebfingerResource>> {
if params.len() != 1 {
2022-11-01 01:57:20 +00:00
eprintln!(
"Too may or too few parameters. Expected 1, got {}",
params.len()
);
2022-10-30 17:36:09 +00:00
return None;
2022-10-30 14:26:34 +00:00
}
2022-11-01 02:53:32 +00:00
let client = match Client::builder().user_agent(user_agent()).build() {
Ok(v) => v,
Err(e) => {
let err = format!("Couldn't build a reqwest client: {:?}", e);
eprintln!("{}", err);
return None;
}
};
2022-10-30 17:36:09 +00:00
if let Some(param) = params.iter().next() {
let url = format!("{}{}", COHOST_ACCOUNT_API_URL, param.0);
eprintln!("making request to {}", url);
2022-11-01 02:53:32 +00:00
match client.get(url).send().await {
2022-11-01 01:57:20 +00:00
Ok(v) => {
match v.status() {
StatusCode::OK => match v.json::<CohostAccount>().await {
Ok(_v) => {
return Some(Json(CohostWebfingerResource::new(
param.0.as_str(),
&ARGS.domain,
2022-11-01 02:21:05 +00:00
&ARGS.base_url,
2022-11-01 01:57:20 +00:00
)));
}
Err(e) => {
eprintln!("Couldn't deserialize Cohost project '{}': {:?}", param.0, e);
}
},
// TODO NORA: Handle possible redirects
s => {
eprintln!("Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", param.0, s);
return None;
}
2022-10-30 17:36:09 +00:00
}
2022-11-01 01:57:20 +00:00
}
Err(e) => {
eprintln!(
"Error making request to Cohost for project '{}': {:?}",
param.0, e
);
2022-10-30 17:36:09 +00:00
return None;
}
};
}
None
2022-10-30 14:26:34 +00:00
}
2022-10-30 17:36:09 +00:00
#[rocket::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Set up the global config
once_cell::sync::Lazy::force(&ARGS);
let _rocket = rocket::build()
2022-11-01 01:57:20 +00:00
.mount(
&ARGS.base_url,
routes![index, webfinger_route, syndication_rss_route],
)
.ignite()
.await?
.launch()
.await?;
2022-10-30 17:36:09 +00:00
Ok(())
2022-10-30 14:26:34 +00:00
}