Basic webfinger proxy for Cohost.

This commit is contained in:
Leonora Tindall 2022-10-30 12:36:09 -05:00
parent 32999e67d4
commit 6a5efb48d9
Signed by: nora
GPG Key ID: 7A8B52EC67E09AAF
7 changed files with 806 additions and 1503 deletions

2107
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,9 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.0.18", features = [ "derive" ] } clap = { version = "4.0.18", features = [ "derive" ] }
eggbug = { version = "0.1.2", features = [ "tokio" ] } eggbug = { version = "0.1.2", features = [ "tokio" ] }
elefren = { version = "0.22.0", features = [ "toml" ] } reqwest = "0.11.12"
tokio = { version = "1.21.2", features = [ "macros", "rt-multi-thread" ] } rocket = { version = "0.5.0-rc.2", features = [ "json" ] }
serde = { version = "1.0.147", features = [ "derive" ] }
tokio = { version = "1.21.2", features = [ "full" ] }
serde_json = "1.0.87"
once_cell = "1.16.0"

View File

@ -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.\""
}

View File

@ -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"
}
]
}

28
src/cohost_account.rs Normal file
View File

@ -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(())
}

View File

@ -1,62 +1,68 @@
extern crate elefren; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use clap::Parser; use clap::Parser;
#[macro_use] extern crate rocket;
use reqwest::StatusCode;
use rocket::{Request, Response};
use rocket::serde::json::Json;
use elefren::prelude::*; mod cohost_account;
use elefren::helpers::toml; mod webfinger;
use elefren::helpers::cli; use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL};
use elefren::entities::event::Event; use webfinger::CohostWebfingerResource;
use eggbug::{Client, Post};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
/// URL of the mastodon instance you're using /// The base URL for the northbound instance
#[arg(short, long, required = true)] #[clap(short, long, required = true)]
instance: String, domain: String,
/// Cohost e-mail /// The base URL for the northbound instance
#[arg(short, long, required = true)] #[clap(short, long, default_value_t = default_base_url() )]
email: String, base_url: String,
/// Cohost password
#[arg(short, long, required = true)]
password: String,
} }
#[tokio::main] fn default_base_url() -> String { "/.well-known/webfinger".into() }
async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse(); static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| {
let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") { Args::parse()
Mastodon::from(data) });
} else {
register(&args.instance)? #[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;
}
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;
}
}; };
// Good! We're now logged into mastodon, check that it worked
mastodon.verify_credentials()?;
// Now log into Cohost
let cohost = Client::new();
let cohost = cohost.login(&args.email, &args.password).await?;
// Now loop over events as they come
for event in mastodon.streaming_user()? {
match event {
} }
None
} }
#[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(()) Ok(())
} }
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)
}

46
src/webfinger.rs Normal file
View File

@ -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(())
}