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]
 | 
					[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"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										102
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										102
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
					@ -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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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