Basic webfinger proxy for Cohost.
This commit is contained in:
parent
32999e67d4
commit
6a5efb48d9
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,9 @@ edition = "2021"
|
|||
[dependencies]
|
||||
clap = { version = "4.0.18", features = [ "derive" ] }
|
||||
eggbug = { version = "0.1.2", features = [ "tokio" ] }
|
||||
elefren = { version = "0.22.0", features = [ "toml" ] }
|
||||
tokio = { version = "1.21.2", features = [ "macros", "rt-multi-thread" ] }
|
||||
|
||||
reqwest = "0.11.12"
|
||||
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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
100
src/main.rs
100
src/main.rs
|
@ -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)?
|
||||
};
|
||||
fn default_base_url() -> String { "/.well-known/webfinger".into() }
|
||||
|
||||
// Good! We're now logged into mastodon, check that it worked
|
||||
mastodon.verify_credentials()?;
|
||||
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| {
|
||||
Args::parse()
|
||||
});
|
||||
|
||||
// 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 {
|
||||
|
||||
}
|
||||
#[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;
|
||||
}
|
||||
};
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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