7 changed files with 763 additions and 1460 deletions
File diff suppressed because it is too large
@ -0,0 +1,6 @@ |
|||
{ |
|||
"projectId": 8891, |
|||
"displayName": "nora (noracodes@weirder.earth)", |
|||
"dek": "queer pagan computer toucher", |
|||
"description": "go follow me on [the fediverse](https://weirder.earth/@noracodes)\r\n\r\nhi! i'm nora. i was born under the [Great Comet of the Millennium](https://en.wikipedia.org/wiki/Comet_Hale%E2%80%93Bopp). i'm a 🦀👩💻\r\nrustacean, 🍄🔮 witch, 📡📻 radio amateur, hacker, synthesist, and general 🚩🏴 leftie nerd who lives on the outskirts of chicago in a little condo with my polycule.\r\n\r\n\"and when the last of those fires let fall / there was no lord in the world at all.\"" |
|||
} |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"subject": "acct:noracodes@example.com", |
|||
"aliases": [ |
|||
"acct:noracodes@cohost.org" |
|||
], |
|||
"links": [ |
|||
{ |
|||
"rel": "http://webfinger.net/rel/profile-page", |
|||
"type": "text/html", |
|||
"href": "https://cohost.org/noracodes" |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,28 @@ |
|||
use serde::Deserialize; |
|||
|
|||
/// The API URL from whence Cohost serves JSON project definitions
|
|||
pub const COHOST_ACCOUNT_API_URL: &str = "https://cohost.org/api/v1/project/"; |
|||
|
|||
#[derive(Debug, Deserialize, PartialEq, Eq)] |
|||
pub struct CohostAccount { |
|||
#[serde(rename = "projectId")] |
|||
project_id: u64, |
|||
#[serde(rename = "displayName")] |
|||
display_name: String, |
|||
dek: String, |
|||
description: String, |
|||
} |
|||
|
|||
#[test] |
|||
fn deserialize_account() -> Result<(), Box<dyn std::error::Error>> { |
|||
let example = include_str!("../samples/cohost/api/v1/project.json"); |
|||
let expected_account = CohostAccount { |
|||
project_id: 8891, |
|||
display_name: "nora (noracodes@weirder.earth)".into(), |
|||
dek: "queer pagan computer toucher".into(), |
|||
description: "go follow me on [the fediverse](https://weirder.earth/@noracodes)\r\n\r\nhi! i'm nora. i was born under the [Great Comet of the Millennium](https://en.wikipedia.org/wiki/Comet_Hale%E2%80%93Bopp). i'm a 🦀👩💻\r\nrustacean, 🍄🔮 witch, 📡📻 radio amateur, hacker, synthesist, and general 🚩🏴 leftie nerd who lives on the outskirts of chicago in a little condo with my polycule.\r\n\r\n\"and when the last of those fires let fall / there was no lord in the world at all.\"".into(), |
|||
}; |
|||
let actual_account: CohostAccount = serde_json::from_str(example)?; |
|||
assert_eq!(expected_account, actual_account); |
|||
Ok(()) |
|||
} |
@ -1,62 +1,68 @@ |
|||
extern crate elefren; |
|||
|
|||
use std::collections::HashMap; |
|||
use std::error::Error; |
|||
use clap::Parser; |
|||
#[macro_use] extern crate rocket; |
|||
use reqwest::StatusCode; |
|||
use rocket::{Request, Response}; |
|||
use rocket::serde::json::Json; |
|||
|
|||
use elefren::prelude::*; |
|||
use elefren::helpers::toml; |
|||
use elefren::helpers::cli; |
|||
use elefren::entities::event::Event; |
|||
use eggbug::{Client, Post}; |
|||
mod cohost_account; |
|||
mod webfinger; |
|||
use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL}; |
|||
use webfinger::CohostWebfingerResource; |
|||
|
|||
#[derive(Parser, Debug)] |
|||
#[command(author, version, about, long_about = None)] |
|||
struct Args { |
|||
/// URL of the mastodon instance you're using
|
|||
#[arg(short, long, required = true)] |
|||
instance: String, |
|||
/// Cohost e-mail
|
|||
#[arg(short, long, required = true)] |
|||
email: String, |
|||
/// Cohost password
|
|||
#[arg(short, long, required = true)] |
|||
password: String, |
|||
/// The base URL for the northbound instance
|
|||
#[clap(short, long, required = true)] |
|||
domain: String, |
|||
/// The base URL for the northbound instance
|
|||
#[clap(short, long, default_value_t = default_base_url() )] |
|||
base_url: String, |
|||
} |
|||
|
|||
#[tokio::main] |
|||
async fn main() -> Result<(), Box<dyn Error>> { |
|||
let args = Args::parse(); |
|||
let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") { |
|||
Mastodon::from(data) |
|||
} else { |
|||
register(&args.instance)? |
|||
}; |
|||
|
|||
// Good! We're now logged into mastodon, check that it worked
|
|||
mastodon.verify_credentials()?; |
|||
fn default_base_url() -> String { "/.well-known/webfinger".into() } |
|||
|
|||
// Now log into Cohost
|
|||
let cohost = Client::new(); |
|||
let cohost = cohost.login(&args.email, &args.password).await?; |
|||
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| { |
|||
Args::parse() |
|||
}); |
|||
|
|||
// Now loop over events as they come
|
|||
for event in mastodon.streaming_user()? { |
|||
match event { |
|||
|
|||
} |
|||
#[get("/.well-known/webfinger?<params..>")] |
|||
async fn webfinger_route(params: HashMap<String, String>) -> Option<Json<CohostWebfingerResource>> { |
|||
if params.len() != 1 { |
|||
eprintln!("Too may or too few parameters. Expected 1, got {}", params.len()); |
|||
return None; |
|||
} |
|||
|
|||
Ok(()) |
|||
if let Some(param) = params.iter().next() { |
|||
let url = format!("{}{}", COHOST_ACCOUNT_API_URL, param.0); |
|||
eprintln!("making request to {}", url); |
|||
match reqwest::get(url).await { |
|||
Ok(v) => match v.status() { |
|||
StatusCode::OK => { |
|||
return Some(Json(CohostWebfingerResource::new(param.0.as_str(), &ARGS.domain))); |
|||
}, |
|||
// 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; |
|||
} |
|||
}; |
|||
} |
|||
None |
|||
} |
|||
|
|||
fn register(instance: &str) -> Result<Mastodon, Box<dyn Error>> { |
|||
let registration = Registration::new(instance) |
|||
.client_name("northbound-train") |
|||
.build()?; |
|||
let mastodon = cli::authenticate(registration)?; |
|||
|
|||
// Save app data for using on the next run.
|
|||
toml::to_file(&*mastodon, "mastodon-data.toml")?; |
|||
|
|||
Ok(mastodon) |
|||
#[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("/", routes![webfinger_route]) |
|||
.ignite().await?.launch().await?; |
|||
Ok(()) |
|||
} |
|||
|
@ -0,0 +1,46 @@ |
|||
use serde::Serialize; |
|||
|
|||
#[derive(Debug, Serialize)] |
|||
pub struct CohostWebfingerResource { |
|||
subject: String, |
|||
aliases: Vec<String>, |
|||
links: Vec<CohostWebfingerProfileLink>, |
|||
} |
|||
|
|||
#[derive(Debug, Serialize)] |
|||
pub struct CohostWebfingerProfileLink { |
|||
rel: String, |
|||
#[serde(rename = "type")] |
|||
t_type: String, |
|||
href: String, |
|||
} |
|||
|
|||
impl CohostWebfingerResource { |
|||
pub fn new<S: AsRef<str>, T: AsRef<str>>(project_id: S, domain: T) -> Self { |
|||
Self { |
|||
subject: format!("acct:{}@{}", project_id.as_ref(), domain.as_ref()), |
|||
aliases: vec![format!("acct:{}@cohost.org", project_id.as_ref())], |
|||
links: vec![CohostWebfingerProfileLink::new(project_id)], |
|||
} |
|||
} |
|||
} |
|||
|
|||
impl CohostWebfingerProfileLink { |
|||
pub fn new<S: AsRef<str>>(project_id: S) -> Self { |
|||
Self { |
|||
rel: "http://webfinger.net/rel/profile-page".into(), |
|||
t_type: "text/html".into(), |
|||
href: format!("https://cohost.org/{}", project_id.as_ref()) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#[test] |
|||
fn serialize_webfinger_resource() -> Result<(), Box<dyn std::error::Error>> { |
|||
let expected_json = include_str!("../samples/northbound-train/.well-known/webfinger"); |
|||
let resource: CohostWebfingerResource = CohostWebfingerResource::new("noracodes", "example.com"); |
|||
let actual_json = serde_json::to_string_pretty(&resource)?; |
|||
println!("{}", actual_json); |
|||
assert_eq!(&expected_json, &actual_json); |
|||
Ok(()) |
|||
} |
Loading…
Reference in new issue