add actor handler

This commit is contained in:
falsycat 2023-05-27 18:32:20 +09:00
parent 5787b16017
commit 9449d5812f
12 changed files with 207 additions and 62 deletions

1
Cargo.lock generated
View File

@ -180,6 +180,7 @@ dependencies = [
"regex",
"rocket",
"serde",
"serde_json",
]
[[package]]

View File

@ -10,3 +10,4 @@ lazy_static = "1.4.0"
regex = "1.8.3"
rocket = { version = "=0.5.0-rc.3", features = ["json"] }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"

3
src/apub.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod actor;
pub use actor::Actor;

29
src/apub/actor.rs Normal file
View File

@ -0,0 +1,29 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Actor {
pub context: Vec<String>,
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub preferred_username: String,
pub inbox: String,
pub outbox: String,
pub public_key: PublicKey,
}
#[derive(Serialize, Deserialize)]
pub struct PublicKey {
pub id: String,
pub owner: String,
pub public_key_pem: String,
}
impl Actor {
pub fn default_context() -> Vec<String> {
vec![
"https://www.w3.org/ns/activitystreams".to_owned(),
"https://w3id.org/security/v1".to_owned(),
]
}
}

View File

@ -1,8 +1,11 @@
pub mod config;
pub mod db;
pub mod user;
pub use config::Config;
pub use db::DB;
pub struct Ctx {
pub cfg: Config,
pub db: Box<dyn DB>,
}

34
src/ctx/config.rs Normal file
View File

@ -0,0 +1,34 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Config {
pub proto: String,
pub domain: String,
pub basepath: String,
}
impl Config {
pub fn default() -> Config {
Config {
proto: "https".to_owned(),
domain: "localhost".to_owned(),
basepath: "".to_owned(),
}
}
pub fn load(path: &str) -> Result<Config, String> {
match std::fs::read_to_string(path) {
Ok(text) => match serde_json::from_str(&text) {
Ok(cfg) => Ok(cfg),
Err(e) => Err(format!("{}", e)),
},
Err(e) => Err(format!("{}", e)),
}
}
pub fn get_baseurl(&self) -> String {
let mut p =
format!("{}://{}/{}", self.proto, self.domain, self.basepath);
while p.ends_with("/") { p.pop(); }
return p;
}
}

View File

@ -5,26 +5,16 @@ pub trait DB : Send + Sync {
fn delete(&self, d: &user::Id) -> Result<(), String>;
fn exists(&self, d: &user::Id) -> Result<bool, String>;
fn get_profile(&self, id: &user::Id) -> Result<user::Profile, String>;
fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result<user::Profile, String>;
fn get_profile(&self, id: &user::Id) -> Result<user::Profile, ReadError>;
fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result<user::Profile, WriteError>;
}
pub struct Null;
impl DB for Null {
fn new(&self, d: &user::Id) -> Result<(), String> {
Err("not implemented".to_owned())
}
fn delete(&self, d: &user::Id) -> Result<(), String> {
Err("not implemented".to_owned())
}
fn exists(&self, d: &user::Id) -> Result<bool, String> {
Err("not implemented".to_owned())
}
fn get_profile(&self, id: &user::Id) -> Result<user::Profile, String> {
Err("not implemented".to_owned())
}
fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result<user::Profile, String> {
Err("not implemented".to_owned())
}
pub enum ReadError {
Missing,
Internal(String),
}
pub enum WriteError {
Missing,
Violation,
Internal(String),
}

View File

@ -1,42 +1,33 @@
use lazy_static::lazy_static;
use regex::Regex;
pub struct Data {
id: Id,
profile: Option<Profile>,
}
pub struct Id {
name: String,
host: Option<String>,
pub name: String,
pub host: Option<String>,
}
pub struct Profile {
name: String,
}
impl Data {
fn get_profile(&self) -> Result<Profile, String> {
Err("not implemented".to_owned())
}
fn set_profile(&self, d: &Profile) -> Result<(), String> {
Err("not implemented".to_owned())
}
pub name: String,
pub private: bool,
}
// hello@world.com -> Id {hello, world.com}
pub fn parse_id(v: &str) -> Result<Id, String> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^([A-Za-z0-9_]+)(?:@([A-Za-z0-9\-\.]+))?$").unwrap();
}
match RE.captures(v) {
Some(caps) => Ok(Id {
name: caps.get(1).unwrap().as_str().to_owned(),
host: caps.get(2).map_or(None, |m| Some(m.as_str().to_owned())),
}),
None => Err("invalid idstr".to_owned()),
}
}
impl Id {
pub fn parse(v: &str) -> Result<Id, String> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^([A-Za-z0-9_]+)(?:@([A-Za-z0-9\-\.]+))?$").unwrap();
}
match RE.captures(v) {
Some(caps) => Ok(Id {
name: caps.get(1).unwrap().as_str().to_owned(),
host: caps.get(2).map_or(None, |m| Some(m.as_str().to_owned())),
}),
None => Err("invalid idstr".to_owned()),
}
}
pub fn local(name: &str) -> Id {
Id {name: name.to_owned(), host: None}
}
fn to_string(&self, default_host: &str) -> String {
format!("{}@{}", self.name, self.host.as_deref().unwrap_or(default_host))
}

View File

@ -1 +1,2 @@
pub mod actor;
pub mod webfinger;

47
src/handler/actor.rs Normal file
View File

@ -0,0 +1,47 @@
use rocket::{get, Responder, State};
use rocket::serde::json::Json;
use crate::apub;
use crate::ctx::db;
use crate::ctx::user;
use crate::ctx::Ctx;
#[derive(Responder)]
pub enum Response {
#[response(status = 200, content_type = "json")]
Found(Json<apub::Actor>),
#[response(status = 404)]
Missing(String),
#[response(status = 500)]
InternalError(String),
}
#[get("/<name>")]
pub fn main(ctx: &State<Ctx>, name: &str) -> Response {
match ctx.db.get_profile(&user::Id::local(name)) {
Ok(profile) => Response::Found(Json(new(ctx, name, &profile))),
Err(db::ReadError::Missing) => Response::Missing(format!("unknown actor: {}", name)),
Err(db::ReadError::Internal(msg)) =>
Response::InternalError(format!("internal error: {}", msg)),
}
}
fn new(ctx: &Ctx, id: &str, p: &user::Profile) -> apub::Actor {
let idurl = format!("{}/{}", ctx.cfg.get_baseurl(), id);
apub::Actor {
context: apub::Actor::default_context(),
id : idurl.clone(),
type_ : "Person".to_owned(),
preferred_username: id.to_owned(),
inbox : format!("{}/inbox", idurl),
outbox : format!("{}/outbox", idurl),
public_key: apub::actor::PublicKey {
id: format!("{}#main-key", idurl),
owner: idurl,
public_key_pem: "".to_owned(),
},
}
}

View File

@ -7,12 +7,21 @@ use crate::ctx::Ctx;
#[derive(Serialize)]
pub struct WebFinger {
subject: String,
aliases: Vec<String>,
subject: String,
aliases: Vec<String>,
links: Vec<Link>,
}
#[derive(Serialize)]
pub struct Link {
rel: String,
#[serde(rename(serialize = "type"))]
type_: String,
href: String,
}
#[derive(Responder)]
pub enum WebFingerResponse {
pub enum Response {
#[response(status = 200, content_type = "json")]
Found(Json<WebFinger>),
@ -27,22 +36,29 @@ pub enum WebFingerResponse {
}
#[get("/?<resource>")]
pub fn main(ctx: &State<Ctx>, resource: &str) -> WebFingerResponse {
pub fn main(ctx: &State<Ctx>, resource: &str) -> Response {
match parse_resource(&resource) {
Ok(("acct", idstr)) | Ok(("", idstr)) => match ctx::user::parse_id(idstr) {
Ok(("acct", idstr)) | Ok(("", idstr)) => match ctx::user::Id::parse(idstr) {
Ok(id) => match ctx.db.exists(&id) {
Ok(true) => WebFingerResponse::Found(Json(WebFinger {
Ok(true) => Response::Found(Json(WebFinger {
subject: resource.to_owned(),
aliases: vec![resource.to_owned()],
links: vec![
Link {
rel: "self".to_owned(),
type_: "application/activity+json".to_owned(),
href: format!("{}/{}", ctx.cfg.get_baseurl(), id.name),
},
],
})),
Ok(false) => WebFingerResponse::Missing(format!("unknown resource: {}", resource)),
Err(msg) => WebFingerResponse::InternalError(format!("error: {}", msg))
Ok(false) => Response::Missing(format!("unknown resource: {}", resource)),
Err(msg) => Response::InternalError(format!("error: {}", msg))
}
Err(msg) => WebFingerResponse::ParseError(format!("error: {}", msg)),
Err(msg) => Response::ParseError(format!("error: {}", msg)),
},
Ok((kind, _)) => WebFingerResponse::ParseError(format!("error: unknown kind, {}", kind)),
Err(msg) => WebFingerResponse::ParseError(format!("error: {}", msg)),
Ok((kind, _)) => Response::ParseError(format!("error: unknown kind, {}", kind)),
Err(msg) => Response::ParseError(format!("error: {}", msg)),
}
}

View File

@ -1,16 +1,45 @@
mod apub;
mod ctx;
mod handler;
use rocket;
use ctx::db;
use ctx::user;
use ctx::Ctx;
use ctx::Config;
#[rocket::launch]
fn launch() -> _ {
let ctx = Ctx {
db: Box::new(ctx::db::Null {}),
cfg: Config::load("fadaway.json").unwrap_or(Config::default()),
db : Box::new(FakeDB {}),
};
rocket::build().
manage(ctx).
mount("/.well-known/webfinger", rocket::routes![handler::webfinger::main])
mount("/.well-known/webfinger", rocket::routes![handler::webfinger::main]).
mount("/", rocket::routes![handler::actor::main])
}
pub struct FakeDB;
impl ctx::DB for FakeDB {
fn new(&self, d: &user::Id) -> Result<(), String> {
Err("not implemented".to_owned())
}
fn delete(&self, d: &user::Id) -> Result<(), String> {
Err("not implemented".to_owned())
}
fn exists(&self, d: &user::Id) -> Result<bool, String> {
Ok(true)
}
fn get_profile(&self, id: &user::Id) -> Result<user::Profile, db::ReadError> {
Ok(user::Profile {
name: id.name.to_owned(),
private: true,
})
}
fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result<user::Profile, db::WriteError> {
Err(db::WriteError::Internal("not implemented".to_owned()))
}
}