use std::fmt::Display;
use super::pun::{actor_type_from_pun, is_valid_pun};
use super::ActorType;
use crate::ap::ap_object::ApObjectType;
use crate::pages::create_base_url;
const AP_SEGMENT: &str = "pub";
const AP_APPLICATION_SEGMENT: &str = "a";
const AP_GROUP_SEGMENT: &str = "g";
const AP_ORGANIZATION_SEGMENT: &str = "o";
const AP_PERSON_SEGMENT: &str = "p";
const AP_SERVICE_SEGMENT: &str = "s";
const AP_SHARED_INBOX_SEGMENT: &str = "sharedInbox";
const AP_INBOX_SEGMENT: &str = "inbox";
const AP_OUTBOX_SEGMENT: &str = "outbox";
const AP_FOLLOWING_SEGMENT: &str = "following";
const AP_FOLLOWERS_SEGMENT: &str = "followers";
const AP_LIKED_SEGMENT: &str = "liked";
const AP_LIKES_SEGMENT: &str = "likes";
const AP_NOTE_SEGMENT: &str = "note";
const AP_ARTICLE_SEGMENT: &str = "article";
const AP_DOCUMENT_SEGMENT: &str = "document";
const AP_IMAGE_SEGMENT: &str = "image";
const AP_VIDEO_SEGMENT: &str = "video";
const AP_AUDIO_SEGMENT: &str = "audio";
const AP_PAGE_SEGMENT: &str = "page";
pub fn create_actor_id(host_name: &str, pun: &str) -> String {
let actor_type = actor_type_from_pun(pun);
let (actor_segment, pun_segment) = match actor_type {
ActorType::Application => (AP_APPLICATION_SEGMENT, &pun[0..pun.len() - 2]),
ActorType::Group => (AP_GROUP_SEGMENT, &pun[0..pun.len() - 2]),
ActorType::Organization => (AP_ORGANIZATION_SEGMENT, &pun[0..pun.len() - 2]),
ActorType::Person => (AP_PERSON_SEGMENT, pun),
ActorType::Service => (AP_SERVICE_SEGMENT, &pun[0..pun.len() - 2]),
};
format!(
"{}/{}/{}/{}",
create_base_url(host_name),
AP_SEGMENT,
actor_segment,
pun_segment
)
}
#[derive(Debug)]
pub enum IdError {
BadHostPrefix,
MissingPath,
InvalidPun,
}
impl Display for IdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdError::BadHostPrefix => write!(f, "Bad Host Prefix"),
IdError::MissingPath => write!(f, "Missing Path"),
IdError::InvalidPun => write!(f, "Invalid Preferred User Name"),
}
}
}
const HTTP_PREFIX: &str = "http://";
const HTTPS_PREFIX: &str = "https://";
pub fn parse_actors_host(actor_id: &str) -> Result<String, IdError> {
if actor_id.starts_with(HTTPS_PREFIX) {
actor_id_host(actor_id, HTTPS_PREFIX)
} else if actor_id.starts_with(HTTP_PREFIX) {
actor_id_host(actor_id, HTTP_PREFIX)
} else {
Err(IdError::BadHostPrefix)
}
}
fn actor_id_host(actor_id: &str, prefix: &str) -> Result<String, IdError> {
let no_prefix = actor_id
.strip_prefix(prefix)
.ok_or(IdError::BadHostPrefix)?;
let parts = no_prefix.split_once('/').ok_or(IdError::MissingPath)?;
if parts.0.len() < 1 {
Err(IdError::MissingPath)
} else {
Ok(parts.0.to_string())
}
}
pub fn parse_actors_pun(actor_id: &str) -> Result<String, IdError> {
let mut parts = actor_id.split('/');
let scheme = parts.next().ok_or(IdError::BadHostPrefix)?;
if !(HTTPS_PREFIX.starts_with(scheme) || HTTP_PREFIX.starts_with(scheme)) {
return Err(IdError::BadHostPrefix);
}
let empty = parts.next().ok_or(IdError::BadHostPrefix)?;
if !empty.is_empty() {
return Err(IdError::BadHostPrefix);
}
let host = parts.next().ok_or(IdError::BadHostPrefix)?;
if host.is_empty() {
return Err(IdError::BadHostPrefix);
}
let ap_segment = parts.next().ok_or(IdError::MissingPath)?;
if ap_segment.is_empty() {
return Err(IdError::MissingPath);
}
let actor_type_segment = parts.next().ok_or(IdError::MissingPath)?;
if actor_type_segment.is_empty() {
return Err(IdError::MissingPath);
}
let pun = parts.next().ok_or(IdError::InvalidPun)?;
if !is_valid_pun(pun, true) {
return Err(IdError::InvalidPun);
}
let after_pun = parts.next();
if after_pun.is_some() {
return Err(IdError::InvalidPun);
}
Ok(pun.to_string())
}
pub fn create_ap_object_id(
host_name: &str,
ap_object_type: &ApObjectType,
local_id: &str,
) -> String {
let ap_object_segment = match ap_object_type {
ApObjectType::Article => AP_ARTICLE_SEGMENT,
ApObjectType::Note => AP_NOTE_SEGMENT,
ApObjectType::Document => AP_DOCUMENT_SEGMENT,
ApObjectType::Image => AP_IMAGE_SEGMENT,
ApObjectType::Video => AP_VIDEO_SEGMENT,
ApObjectType::Audio => AP_AUDIO_SEGMENT,
ApObjectType::Page => AP_PAGE_SEGMENT,
};
format!(
"{}/{}/{}/{}",
create_base_url(host_name),
AP_SEGMENT,
ap_object_segment,
local_id
)
}
pub fn parse_ap_object_id_host(ap_object_id: &str) -> Result<String, IdError> {
if ap_object_id.starts_with(HTTPS_PREFIX) {
ap_object_id_host(ap_object_id, HTTPS_PREFIX)
} else if ap_object_id.starts_with(HTTP_PREFIX) {
ap_object_id_host(ap_object_id, HTTP_PREFIX)
} else {
Err(IdError::BadHostPrefix)
}
}
fn ap_object_id_host(ap_object_id: &str, prefix: &str) -> Result<String, IdError> {
let no_prefix = ap_object_id
.strip_prefix(prefix)
.ok_or(IdError::BadHostPrefix)?;
let parts = no_prefix.split_once('/').ok_or(IdError::MissingPath)?;
if parts.0.len() < 1 {
Err(IdError::MissingPath)
} else {
Ok(parts.0.to_string())
}
}
pub fn create_inbox(id: &str) -> String {
format!("{}/{}", id, AP_INBOX_SEGMENT)
}
pub fn create_outbox(id: &str) -> String {
format!("{}/{}", id, AP_OUTBOX_SEGMENT)
}
pub fn create_following(id: &str) -> String {
format!("{}/{}", id, AP_FOLLOWING_SEGMENT)
}
pub fn create_followers(id: &str) -> String {
format!("{}/{}", id, AP_FOLLOWERS_SEGMENT)
}
pub fn create_liked(id: &str) -> String {
format!("{}/{}", id, AP_LIKED_SEGMENT)
}
pub fn create_likes(id: &str) -> String {
format!("{}/{}", id, AP_LIKES_SEGMENT)
}
pub fn create_shared_inbox(host_name: &str) -> String {
format!(
"{}/{}/{}",
create_base_url(host_name),
AP_SEGMENT,
AP_SHARED_INBOX_SEGMENT
)
}
#[cfg(test)]
#[allow(non_snake_case)]
mod id_tests {
use dialtone_test_util::test_constants::TEST_HOSTNAME;
use dialtone_test_util::test_constants::TEST_NOROLEUSER_PUN;
use super::create_actor_id;
use super::parse_actors_host;
use super::parse_actors_pun;
use super::parse_ap_object_id_host;
use crate::ap::ap_object::ApObjectType;
use crate::ap::id::create_ap_object_id;
use crate::ap::pun::create_preferred_user_name;
#[test]
fn GIVEN_person_pun_WHEN_create_actor_id_THEN_segment_is_p() {
let host_name = "example.com";
let name = "testy_mctestalot";
let pun = create_preferred_user_name(name, &crate::ap::ActorType::Person);
let actor_id = create_actor_id(host_name, &pun);
assert_eq!("https://example.com/pub/p/testy_mctestalot", actor_id);
}
#[test]
fn GIVEN_group_pun_WHEN_create_actor_id_THEN_segment_is_g() {
let host_name = "example.com";
let name = "testy_mctestalot";
let pun = create_preferred_user_name(name, &crate::ap::ActorType::Group);
let actor_id = create_actor_id(host_name, &pun);
assert_eq!("https://example.com/pub/g/testy_mctestalot", actor_id);
}
#[test]
fn GIVEN_service_pun_WHEN_create_actor_id_THEN_segment_is_s() {
let host_name = "example.com";
let name = "testy_mctestalot";
let pun = create_preferred_user_name(name, &crate::ap::ActorType::Service);
let actor_id = create_actor_id(host_name, &pun);
assert_eq!("https://example.com/pub/s/testy_mctestalot", actor_id);
}
#[test]
fn GIVEN_application_pun_WHEN_create_actor_id_THEN_segment_is_a() {
let host_name = "example.com";
let name = "testy_mctestalot";
let pun = create_preferred_user_name(name, &crate::ap::ActorType::Application);
let actor_id = create_actor_id(host_name, &pun);
assert_eq!("https://example.com/pub/a/testy_mctestalot", actor_id);
}
#[test]
fn GIVEN_organization_pun_WHEN_create_actor_id_THEN_segment_is_o() {
let host_name = "example.com";
let name = "testy_mctestalot";
let pun = create_preferred_user_name(name, &crate::ap::ActorType::Organization);
let actor_id = create_actor_id(host_name, &pun);
assert_eq!("https://example.com/pub/o/testy_mctestalot", actor_id);
}
#[test]
fn GIVEN_person_actor_id_WHEN_parse_pun_THEN_return_pun() {
let actor_id = create_actor_id(TEST_HOSTNAME, TEST_NOROLEUSER_PUN);
let pun = parse_actors_pun(&actor_id).unwrap();
assert_eq!(pun, TEST_NOROLEUSER_PUN);
}
#[test]
fn GIVEN_actor_id_with_no_host_WHEN_parse_pun_THEN_parsing_error() {
let actor_id = "https:///p/test";
let pun = parse_actors_pun(&actor_id);
assert!(pun.is_err());
}
#[test]
fn GIVEN_actor_id_with_no_bad_pun_WHEN_parse_pun_THEN_parsing_error() {
let actor_id = "https://example.com/p/test::::";
let pun = parse_actors_pun(&actor_id);
assert!(pun.is_err());
}
#[test]
fn GIVEN_actor_id_with_stuff_after_pun_WHEN_parse_pun_THEN_parsing_error() {
let actor_id = "https://example.com/ap/p/test/stuff";
let pun = parse_actors_pun(&actor_id);
assert!(pun.is_err());
}
#[test]
fn test_create_ap_object_ids() {
let host_name = "example.com";
let local_part = "a_thing_i_did";
assert_eq!(
"https://example.com/pub/note/a_thing_i_did",
create_ap_object_id(host_name, &ApObjectType::Note, local_part)
);
assert_eq!(
"https://example.com/pub/article/a_thing_i_did",
create_ap_object_id(host_name, &ApObjectType::Article, local_part)
);
assert_eq!(
"https://example.com/pub/document/a_thing_i_did",
create_ap_object_id(host_name, &ApObjectType::Document, local_part)
);
}
#[test]
fn GIVEN_ap_object_id_WHEN_parse_host_THEN_ok() {
let local_part = "a_thing_i_did";
let id = create_ap_object_id(TEST_HOSTNAME, &ApObjectType::Note, local_part);
let action = parse_ap_object_id_host(&id);
assert!(action.is_ok());
assert_eq!(action.unwrap(), TEST_HOSTNAME);
}
#[test]
fn GIVEN_good_actor_id_with_https_WHEN_find_actors_host_THEN_ok() {
let action = parse_actors_host("https://example.com/pub/p/foo");
assert!(action.is_ok());
}
#[test]
fn GIVEN_good_actor_id_with_http_WHEN_find_actors_host_THEN_ok() {
let action = parse_actors_host("http://example.com/pub/p/foo");
assert!(action.is_ok());
}
#[test]
fn GIVEN_actor_id_with_no_scheme_WHEN_find_actors_host_THEN_error() {
let action = parse_actors_host("example.com/pub/p/foo");
assert!(action.is_err());
}
#[test]
fn GIVEN_actor_id_with_no_host_WHEN_find_actors_host_THEN_error() {
let action = parse_actors_host("http:///pub/p/foo");
assert!(action.is_err());
}
}