|
|
@ -12,7 +12,7 @@ mod cohost_posts; |
|
|
|
mod syndication; |
|
|
|
mod webfinger; |
|
|
|
use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL}; |
|
|
|
use cohost_posts::{cohost_posts_api_url, CohostPostsPage}; |
|
|
|
use cohost_posts::{cohost_posts_api_url, CohostPost, CohostPostsPage}; |
|
|
|
use webfinger::CohostWebfingerResource; |
|
|
|
|
|
|
|
#[derive(Parser, Debug)] |
|
|
@ -46,6 +46,12 @@ fn index() -> RawHtml<&'static str> { |
|
|
|
RawHtml(include_str!("../static/index.html")) |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Responder)] |
|
|
|
#[response(content_type = "text/markdown")] |
|
|
|
struct MdResponse { |
|
|
|
inner: String, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Responder)] |
|
|
|
#[response(content_type = "application/rss+xml")] |
|
|
|
struct RssResponse { |
|
|
@ -61,149 +67,182 @@ enum ErrorResponse { |
|
|
|
InternalError(String), |
|
|
|
} |
|
|
|
|
|
|
|
#[get("/<project>/feed.rss?<page>")] |
|
|
|
async fn syndication_rss_route( |
|
|
|
project: &str, |
|
|
|
page: Option<u64>, |
|
|
|
) -> Result<RssResponse, ErrorResponse> { |
|
|
|
let page = page.unwrap_or(0); |
|
|
|
let project_url = format!("{}{}", COHOST_ACCOUNT_API_URL, project); |
|
|
|
let posts_url = cohost_posts_api_url(project, page); |
|
|
|
async fn get_post_from_page( |
|
|
|
client: &mut Client, |
|
|
|
project_id: &str, |
|
|
|
post_id: u64, |
|
|
|
) -> Result<CohostPost, ErrorResponse> { |
|
|
|
let mut page = 0; |
|
|
|
loop { |
|
|
|
let new_page = get_page_data(client, project_id, page).await?; |
|
|
|
if new_page.items.is_empty() { |
|
|
|
// Once there are no posts, we're done.
|
|
|
|
return Err(ErrorResponse::NotFound( |
|
|
|
"End of posts reached, ID not found.".into(), |
|
|
|
)); |
|
|
|
} else { |
|
|
|
page += 1; |
|
|
|
if let Some(post) = new_page.items.into_iter().find(|post| post.id == post_id) { |
|
|
|
return Ok(post); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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)); |
|
|
|
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) |
|
|
|
} |
|
|
|
|
|
|
|
eprintln!("making request to {}", project_url); |
|
|
|
let project_data: CohostAccount = match client.get(project_url).send().await { |
|
|
|
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::<CohostAccount>().await { |
|
|
|
Ok(a) => a, |
|
|
|
StatusCode::OK => match v.json::<CohostPostsPage>().await { |
|
|
|
Ok(page_data) => Ok(page_data), |
|
|
|
Err(e) => { |
|
|
|
let err = format!("Couldn't deserialize Cohost project '{}': {:?}", project, 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 Cohost project '{}'; got {:?} instead.", |
|
|
|
project, 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 project '{}': {:?}", |
|
|
|
project, e |
|
|
|
"Error making request to Cohost for posts for project '{}': {:?}", |
|
|
|
project_id, e |
|
|
|
); |
|
|
|
eprintln!("{}", err); |
|
|
|
return Err(ErrorResponse::InternalError(err)); |
|
|
|
} |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
eprintln!("making request to {}", posts_url); |
|
|
|
match client.get(posts_url).send().await { |
|
|
|
#[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(), |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
#[get("/<project>/<id>")] |
|
|
|
async fn post_md_route(project: &str, id: u64) -> Result<MdResponse, ErrorResponse> { |
|
|
|
let mut client = get_client()?; |
|
|
|
|
|
|
|
let _project_data = get_project_data(&mut client, project).await?; |
|
|
|
let post_data = get_post_from_page(&mut client, project, id).await?; |
|
|
|
Ok(MdResponse { |
|
|
|
inner: post_data.plain_body, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
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::<CohostPostsPage>().await { |
|
|
|
Ok(page_data) => { |
|
|
|
return Ok(RssResponse { |
|
|
|
inner: syndication::channel_for_posts_page( |
|
|
|
project, |
|
|
|
page, |
|
|
|
project_data, |
|
|
|
page_data, |
|
|
|
) |
|
|
|
.to_string(), |
|
|
|
}); |
|
|
|
} |
|
|
|
StatusCode::OK => match v.json::<CohostAccount>().await { |
|
|
|
Ok(a) => Ok(a), |
|
|
|
Err(e) => { |
|
|
|
let err = format!( |
|
|
|
"Couldn't deserialize Cohost posts page for '{}': {:?}", |
|
|
|
project, e |
|
|
|
"Couldn't deserialize Cohost project '{}': {:?}", |
|
|
|
project_id, e |
|
|
|
); |
|
|
|
eprintln!("{}", err); |
|
|
|
return Err(ErrorResponse::InternalError(err)); |
|
|
|
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); |
|
|
|
let err = format!( |
|
|
|
"Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", |
|
|
|
project_id, s |
|
|
|
); |
|
|
|
eprintln!("{}", err); |
|
|
|
return Err(ErrorResponse::NotFound(err)); |
|
|
|
Err(ErrorResponse::NotFound(err)) |
|
|
|
} |
|
|
|
}, |
|
|
|
Err(e) => { |
|
|
|
let err = format!( |
|
|
|
"Error making request to Cohost for posts for project '{}': {:?}", |
|
|
|
project, e |
|
|
|
"Error making request to Cohost for project '{}': {:?}", |
|
|
|
project_id, e |
|
|
|
); |
|
|
|
eprintln!("{}", err); |
|
|
|
return Err(ErrorResponse::InternalError(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>) -> Option<Json<CohostWebfingerResource>> { |
|
|
|
async fn webfinger_route( |
|
|
|
params: HashMap<String, String>, |
|
|
|
) -> Result<Json<CohostWebfingerResource>, ErrorResponse> { |
|
|
|
if params.len() != 1 { |
|
|
|
eprintln!( |
|
|
|
let err = format!( |
|
|
|
"Too may or too few parameters. Expected 1, got {}", |
|
|
|
params.len() |
|
|
|
); |
|
|
|
return None; |
|
|
|
eprintln!("{}", err); |
|
|
|
return Err(ErrorResponse::InternalError(err)); |
|
|
|
} |
|
|
|
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; |
|
|
|
} |
|
|
|
}; |
|
|
|
let mut client = get_client()?; |
|
|
|
if let Some(param) = params.iter().next() { |
|
|
|
let url = format!("{}{}", COHOST_ACCOUNT_API_URL, param.0); |
|
|
|
eprintln!("making request to {}", url); |
|
|
|
match client.get(url).send().await { |
|
|
|
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, |
|
|
|
&ARGS.base_url, |
|
|
|
))); |
|
|
|
} |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
eprintln!( |
|
|
|
"Error making request to Cohost for project '{}': {:?}", |
|
|
|
param.0, e |
|
|
|
); |
|
|
|
return None; |
|
|
|
} |
|
|
|
}; |
|
|
|
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())) |
|
|
|
} |
|
|
|
None |
|
|
|
} |
|
|
|
|
|
|
|
#[rocket::main] |
|
|
@ -213,7 +252,7 @@ async fn main() -> Result<(), Box<dyn Error>> { |
|
|
|
let _rocket = rocket::build() |
|
|
|
.mount( |
|
|
|
&ARGS.base_url, |
|
|
|
routes![index, webfinger_route, syndication_rss_route], |
|
|
|
routes![index, webfinger_route, syndication_rss_route, post_md_route], |
|
|
|
) |
|
|
|
.ignite() |
|
|
|
.await? |
|
|
|