~vpzom/lotide

0c638861d290d332008d9164a867ab2d6d1a241f — Colin Reeder 9 months ago 84881b7
Update to alpha version of activitystreams
M Cargo.lock => Cargo.lock +10 -9
@@ 2,11 2,10 @@
# It is not intended for manual editing.
[[package]]
name = "activitystreams"
version = "0.6.2"
version = "0.7.0-alpha.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464cb473bfb402b857cc15b1153974c203a43f1485da4dda15cd17a738548958"
checksum = "e3490e8e9d7744aada19fb2fb4e2564f8c22fd080a3561093ac91ed7d10bfe78"
dependencies = [
 "activitystreams-derive",
 "chrono",
 "mime",
 "serde",


@@ 16,14 15,14 @@ dependencies = [
]

[[package]]
name = "activitystreams-derive"
version = "0.6.1"
name = "activitystreams-ext"
version = "0.1.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4"
checksum = "bb8e19a0810cc25df3535061a08b7d8f8a734d309ea4411c57a9767e4a2ffa0e"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "activitystreams",
 "serde",
 "serde_json",
]

[[package]]


@@ 814,6 813,7 @@ name = "lotide"
version = "0.5.0-pre"
dependencies = [
 "activitystreams",
 "activitystreams-ext",
 "async-trait",
 "bcrypt",
 "bytes",


@@ 1764,6 1764,7 @@ dependencies = [
 "idna",
 "matches",
 "percent-encoding",
 "serde",
]

[[package]]

M Cargo.toml => Cargo.toml +3 -2
@@ 12,7 12,6 @@ hyper = "0.13.5"
hyper-tls = "0.4.1"
tokio-postgres = { version = "0.5.3", features = ["with-uuid-0_8", "with-chrono-0_4", "with-serde_json-1"] }
tokio = { version = "0.2.18", features = ["macros"] }
activitystreams = "0.6.2"
trout = "0.2.0"
deadpool-postgres = "0.5.5"
serde_json = "1.0.53"


@@ 23,7 22,7 @@ serde = "1.0.111"
uuid = { version = "0.8.1", features = ["v4"] }
headers = "0.3.2"
chrono = "0.4.11"
url = "2.1.1"
url = { version = "2.1.1", features = ["serde"] }
bytes = "0.5.4"
either = "1.5.3"
openssl = "0.10.29"


@@ 40,3 39,5 @@ pulldown-cmark = "0.7.2"
fluent = "0.12.0"
fluent-langneg = "0.13.0"
unic-langid = { version = "0.9.0", features = ["macros"] }
activitystreams = "0.7.0-alpha.3"
activitystreams-ext = "0.1.0-alpha.2"

M src/apub_util.rs => src/apub_util.rs +579 -678
@@ 1,7 1,9 @@
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, ThingLocalRef, UserLocalID};
use serde_derive::{Deserialize, Serialize};
use crate::{BaseURL, CommentLocalID, CommunityLocalID, PostLocalID, ThingLocalRef, UserLocalID};
use activitystreams::prelude::*;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
use std::ops::Deref;
use std::sync::Arc;

pub const ACTIVITY_TYPE: &str = "application/activity+json";


@@ 24,50 26,62 @@ impl<T: Clone> Verified<T> {
        self.0
    }
}
impl Verified<activitystreams::activity::ActivityBox> {
    pub fn into_concrete_activity<
        T: activitystreams::Activity + serde::de::DeserializeOwned + Clone,
    >(
        self,
    ) -> Result<Verified<T>, std::io::Error> {
        Ok(Verified(self.0.into_concrete()?))
    }
}
impl<
        T: std::convert::TryInto<activitystreams::object::ObjectBox, Error = std::io::Error> + Clone,
    > Verified<T>
{
    pub fn try_box_object(
        self,
    ) -> Result<Verified<activitystreams::object::ObjectBox>, std::io::Error> {
        Ok(Verified(self.0.try_into()?))
    }
}
impl Verified<activitystreams::object::ObjectBox> {
    pub fn into_concrete_object<
        T: activitystreams::Object + serde::de::DeserializeOwned + Clone,
    >(
        self,
    ) -> Result<Verified<T>, std::io::Error> {
        Ok(Verified(self.0.into_concrete()?))
    }
}

pub struct Contained<'a, T: activitystreams::Base + Clone>(pub Cow<'a, Verified<T>>);
impl<'a, T: activitystreams::Base + Clone> std::ops::Deref for Contained<'a, T> {
pub struct Contained<'a, T: activitystreams::markers::Base + Clone>(pub Cow<'a, Verified<T>>);
impl<'a, T: activitystreams::markers::Base + Clone> std::ops::Deref for Contained<'a, T> {
    type Target = Verified<T>;

    fn deref(&self) -> &Self::Target {
        self.0.as_ref()
    }
}
impl<'a, T: activitystreams::Base + Clone> Contained<'a, T> {
impl<'a, T: activitystreams::markers::Base + Clone> Contained<'a, T> {
    pub fn into_inner(self) -> Cow<'a, Verified<T>> {
        self.0
    }
    pub fn with_owned(self) -> Contained<'static, T> {
        Contained(Cow::Owned(self.0.into_owned()))
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum KnownObject {
    Accept(activitystreams::activity::Accept),
    Announce(activitystreams::activity::Announce),
    Create(activitystreams::activity::Create),
    Delete(activitystreams::activity::Delete),
    Follow(activitystreams::activity::Follow),
    Like(activitystreams::activity::Like),
    Undo(activitystreams::activity::Undo),
    Update(activitystreams::activity::Update),
    Person(
        activitystreams_ext::Ext1<
            activitystreams::actor::ApActor<activitystreams::actor::Person>,
            PublicKeyExtension<'static>,
        >,
    ),
    Group(
        activitystreams_ext::Ext1<
            activitystreams::actor::ApActor<activitystreams::actor::Group>,
            PublicKeyExtension<'static>,
        >,
    ),
    Page(activitystreams::object::Page),
    Note(activitystreams::object::Note),
}

#[derive(Deserialize)]
pub struct JustMaybeAPID {
    id: Option<BaseURL>,
}

#[derive(Deserialize)]
pub struct JustActor {
    actor: activitystreams::primitives::OneOrMany<activitystreams::base::AnyBase>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey<'a> {
    pub id: Cow<'a, str>,


@@ 83,127 97,135 @@ pub struct PublicKeyExtension<'a> {
    pub public_key: Option<PublicKey<'a>>,
}

impl<'a, T: activitystreams::actor::Actor> activitystreams::ext::Extension<T>
    for PublicKeyExtension<'a>
{
pub fn get_local_shared_inbox(host_url_apub: &BaseURL) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut().push("inbox");
    res
}

pub fn get_local_post_apub_id(post: PostLocalID, host_url_apub: &str) -> String {
    format!("{}/posts/{}", host_url_apub, post)
pub fn get_local_post_apub_id(post: PostLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut()
        .extend(&["posts", &post.to_string()]);
    res
}

pub fn get_local_post_like_apub_id(
    post_local_id: PostLocalID,
    user: UserLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/likes/{}",
        crate::apub_util::get_local_post_apub_id(post_local_id, &host_url_apub),
        user,
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = crate::apub_util::get_local_post_apub_id(post_local_id, &host_url_apub);
    res.path_segments_mut()
        .extend(&["likes", &user.to_string()]);
    res
}

pub fn get_local_comment_apub_id(comment: CommentLocalID, host_url_apub: &str) -> String {
    format!("{}/comments/{}", host_url_apub, comment)
pub fn get_local_comment_apub_id(comment: CommentLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut()
        .extend(&["comments", &comment.to_string()]);
    res
}

pub fn get_local_comment_like_apub_id(
    comment_local_id: CommentLocalID,
    user: UserLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/likes/{}",
        crate::apub_util::get_local_comment_apub_id(comment_local_id, &host_url_apub),
        user
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = crate::apub_util::get_local_comment_apub_id(comment_local_id, &host_url_apub);
    res.path_segments_mut()
        .extend(&["likes", &user.to_string()]);
    res
}

pub fn get_local_person_apub_id(person: UserLocalID, host_url_apub: &str) -> String {
    format!("{}/users/{}", host_url_apub, person)
pub fn get_local_person_apub_id(person: UserLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut()
        .extend(&["users", &person.to_string()]);
    res
}

pub fn get_local_person_outbox_apub_id(person: UserLocalID, host_url_apub: &str) -> String {
    format!("{}/outbox", get_local_person_apub_id(person, host_url_apub))
pub fn get_local_person_outbox_apub_id(person: UserLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = get_local_person_apub_id(person, host_url_apub);
    res.path_segments_mut().push(&person.to_string());
    res
}

pub fn get_local_person_outbox_page_apub_id(
    person: UserLocalID,
    page: &crate::TimestampOrLatest,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/page/{}",
        get_local_person_outbox_apub_id(person, host_url_apub),
        page
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_person_outbox_apub_id(person, host_url_apub);
    res.path_segments_mut().extend(&["page", &page.to_string()]);
    res
}

pub fn get_local_community_apub_id(community: CommunityLocalID, host_url_apub: &str) -> String {
    format!("{}/communities/{}", host_url_apub, community)
pub fn get_local_community_apub_id(
    community: CommunityLocalID,
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut()
        .extend(&["communities", &community.to_string()]);
    res
}

pub fn get_local_community_outbox_apub_id(
    community: CommunityLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/outbox",
        get_local_community_apub_id(community, host_url_apub)
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_apub_id(community, host_url_apub);
    res.path_segments_mut().push("outbox");
    res
}

pub fn get_local_community_outbox_page_apub_id(
    community: CommunityLocalID,
    page: &crate::TimestampOrLatest,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/page/{}",
        get_local_community_outbox_apub_id(community, host_url_apub),
        page
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_outbox_apub_id(community, host_url_apub);
    res.path_segments_mut().extend(&["page", &page.to_string()]);
    res
}

pub fn get_local_community_follow_apub_id(
    community: CommunityLocalID,
    follower: UserLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/communities/{}/followers/{}",
        host_url_apub, community, follower
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_apub_id(community, host_url_apub);
    res.path_segments_mut()
        .extend(&["followers", &follower.to_string()]);
    res
}

pub fn get_local_person_pubkey_apub_id(person: UserLocalID, host_url_apub: &str) -> String {
    format!(
        "{}#main-key",
        get_local_person_apub_id(person, host_url_apub)
    )
pub fn get_local_person_pubkey_apub_id(person: UserLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = get_local_person_apub_id(person, host_url_apub);
    res.set_fragment(Some("main-key"));
    res
}

pub fn get_local_community_pubkey_apub_id(
    community: CommunityLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}#main-key",
        get_local_community_apub_id(community, host_url_apub)
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_apub_id(community, host_url_apub);
    res.set_fragment(Some("main-key"));
    res
}

pub fn get_local_follow_apub_id(
    community: CommunityLocalID,
    follower: UserLocalID,
    host_url_apub: &str,
) -> String {
    format!(
        "{}/followers/{}",
        get_local_community_apub_id(community, host_url_apub),
        follower
    )
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_apub_id(community, host_url_apub);
    res.path_segments_mut()
        .extend(&["followers", &follower.to_string()]);
    res
}

pub fn now_http_date() -> hyper::header::HeaderValue {


@@ 271,11 293,11 @@ pub fn require_containment(object_id: &url::Url, actor_id: &url::Url) -> Result<
    }
}

pub async fn fetch_ap_object(
    ap_id: &str,
pub async fn fetch_ap_object_raw(
    ap_id: &url::Url,
    http_client: &crate::HttpClient,
) -> Result<serde_json::Value, crate::Error> {
    let mut current_id = hyper::Uri::try_from(ap_id)?;
    let mut current_id = hyper::Uri::try_from(ap_id.as_str())?;
    for _ in 0..3u8 {
        // avoid infinite loop in malicious or broken cases
        let res = crate::res_to_error(


@@ 310,57 332,56 @@ pub async fn fetch_ap_object(
    Err(crate::Error::InternalStrStatic("Recursion depth exceeded"))
}

pub async fn fetch_ap_object(
    ap_id: &url::Url,
    http_client: &crate::HttpClient,
) -> Result<Verified<KnownObject>, crate::Error> {
    let value = fetch_ap_object_raw(ap_id, http_client).await?;
    let value: KnownObject = serde_json::from_value(value)?;
    Ok(Verified(value))
}

pub async fn fetch_actor(
    req_ap_id: &str,
    req_ap_id: &url::Url,
    db: &tokio_postgres::Client,
    http_client: &crate::HttpClient,
) -> Result<ActorLocalInfo, crate::Error> {
    let obj = fetch_ap_object(req_ap_id, http_client).await?;
    let ap_id = req_ap_id;

    match obj.get("type").and_then(serde_json::Value::as_str) {
        Some("Person") => {
            let person: activitystreams::ext::Ext<
                activitystreams::ext::Ext<
                    activitystreams::actor::Person,
                    activitystreams::actor::properties::ApActorProperties,
                >,
                crate::apub_util::PublicKeyExtension<'_>,
            > = serde_json::from_value(obj)?;

            let ap_id = person.base.base.object_props.id.as_ref().unwrap().as_str();
    match obj.deref() {
        KnownObject::Person(person) => {
            let username = person
                .base
                .extension
                .get_preferred_username()
                .map(|x| x.as_str())
                .or_else(|| person.as_ref().get_name_xsd_string().map(|x| x.as_str()))
                .preferred_username()
                .or_else(|| {
                    person
                        .name()
                        .and_then(|maybe| maybe.iter().filter_map(|x| x.as_xsd_string()).next())
                })
                .unwrap_or("");
            let inbox = person.base.extension.inbox.as_str();
            let inbox = person.inbox_unchecked().as_str();
            let shared_inbox = person
                .base
                .extension
                .get_endpoints()
                .and_then(|endpoints| endpoints.get_shared_inbox())
                .endpoints_unchecked()
                .and_then(|endpoints| endpoints.shared_inbox)
                .map(|url| url.as_str());
            let public_key = person
                .extension
                .ext_one
                .public_key
                .as_ref()
                .map(|key| key.public_key_pem.as_bytes());
            let public_key_sigalg = person
                .extension
                .ext_one
                .public_key
                .as_ref()
                .and_then(|key| key.signature_algorithm.as_deref());
            let description = person
                .as_ref()
                .get_summary_xsd_string()
                .map(|x| x.as_str())
                .summary()
                .and_then(|maybe| maybe.iter().filter_map(|x| x.as_xsd_string()).next())
                .unwrap_or("");

            let id = UserLocalID(db.query_one(
                "INSERT INTO person (username, local, created_local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description) VALUES ($1, FALSE, localtimestamp, $2, $3, $4, $5, $6, $7) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description=$7 RETURNING id",
                &[&username, &ap_id, &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description],
                &[&username, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description],
            ).await?.get(0));

            Ok(ActorLocalInfo::User {


@@ 371,45 392,37 @@ pub async fn fetch_actor(
                }),
            })
        }
        Some("Group") => {
            let group: activitystreams::ext::Ext<
                activitystreams::ext::Ext<
                    activitystreams::actor::Group,
                    activitystreams::actor::properties::ApActorProperties,
                >,
                crate::apub_util::PublicKeyExtension<'_>,
            > = serde_json::from_value(obj)?;

            let ap_id = group.base.base.object_props.id.as_ref().unwrap().as_str();
        KnownObject::Group(group) => {
            let name = group
                .base
                .extension
                .get_preferred_username()
                .map(|x| x.as_str())
                .or_else(|| group.as_ref().get_name_xsd_string().map(|x| x.as_str()))
                .preferred_username()
                .or_else(|| {
                    group
                        .name()
                        .and_then(|maybe| maybe.iter().filter_map(|x| x.as_xsd_string()).next())
                })
                .unwrap_or("");
            let description = group.as_ref().get_summary_xsd_string().map(|x| x.as_str());
            let inbox = group.base.extension.inbox.as_str();
            let description = group
                .summary()
                .and_then(|maybe| maybe.iter().filter_map(|x| x.as_xsd_string()).next());
            let inbox = group.inbox_unchecked().as_str();
            let shared_inbox = group
                .base
                .extension
                .get_endpoints()
                .and_then(|endpoints| endpoints.get_shared_inbox())
                .endpoints_unchecked()
                .and_then(|endpoints| endpoints.shared_inbox)
                .map(|url| url.as_str());
            let public_key = group
                .extension
                .ext_one
                .public_key
                .as_ref()
                .map(|key| key.public_key_pem.as_bytes());
            let public_key_sigalg = group
                .extension
                .ext_one
                .public_key
                .as_ref()
                .and_then(|key| key.signature_algorithm.as_deref());

            let id = CommunityLocalID(db.query_one(
                "INSERT INTO community (name, local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description) VALUES ($1, FALSE, $2, $3, $4, $5, $6, $7) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description=$7 RETURNING id",
                &[&name, &ap_id, &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description],
                &[&name, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description],
            ).await?.get(0));

            Ok(ActorLocalInfo::Community {


@@ 425,14 438,14 @@ pub async fn fetch_actor(
}

pub async fn get_or_fetch_user_local_id(
    ap_id: &str,
    ap_id: &url::Url,
    db: &tokio_postgres::Client,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
    http_client: &crate::HttpClient,
) -> Result<UserLocalID, crate::Error> {
    if ap_id.starts_with(host_url_apub) {
        if ap_id[host_url_apub.len()..].starts_with("/users/") {
            Ok(ap_id[(host_url_apub.len() + 7)..].parse()?)
    if ap_id.as_str().starts_with(host_url_apub.as_str()) {
        if ap_id.as_str()[host_url_apub.as_str().len()..].starts_with("/users/") {
            Ok(ap_id.as_str()[(host_url_apub.as_str().len() + 7)..].parse()?)
        } else {
            Err(crate::Error::InternalStr(format!(
                "Unrecognized local AP ID: {:?}",


@@ 441,7 454,7 @@ pub async fn get_or_fetch_user_local_id(
        }
    } else {
        match db
            .query_opt("SELECT id FROM person WHERE ap_id=$1", &[&ap_id])
            .query_opt("SELECT id FROM person WHERE ap_id=$1", &[&ap_id.as_str()])
            .await?
        {
            Some(row) => Ok(UserLocalID(row.get(0))),


@@ 535,8 548,8 @@ pub async fn fetch_or_create_local_community_privkey(
pub async fn fetch_or_create_local_actor_privkey(
    actor_ref: crate::ActorLocalRef,
    db: &tokio_postgres::Client,
    host_url_apub: &str,
) -> Result<(openssl::pkey::PKey<openssl::pkey::Private>, String), crate::Error> {
    host_url_apub: &BaseURL,
) -> Result<(openssl::pkey::PKey<openssl::pkey::Private>, BaseURL), crate::Error> {
    Ok(match actor_ref {
        crate::ActorLocalRef::Person(id) => (
            fetch_or_create_local_user_privkey(id, db).await?,


@@ 568,7 581,7 @@ pub fn spawn_enqueue_send_community_follow(
    crate::spawn_task(async move {
        let db = ctx.db_pool.get().await?;

        let (community_ap_id, community_inbox): (String, String) = {
        let (community_ap_id, community_inbox): (url::Url, url::Url) = {
            let row = db
                .query_one(
                    "SELECT local, ap_id, ap_inbox FROM community WHERE id=$1",


@@ 580,12 593,12 @@ pub fn spawn_enqueue_send_community_follow(
                // no need to send follows to ourself
                return Ok(());
            } else {
                let ap_id = row.get(1);
                let ap_inbox = row.get(2);
                let ap_id: Option<&str> = row.get(1);
                let ap_inbox: Option<&str> = row.get(2);

                (if let Some(ap_id) = ap_id {
                    if let Some(ap_inbox) = ap_inbox {
                        Some((ap_id, ap_inbox))
                        Some((ap_id.parse()?, ap_inbox.parse()?))
                    } else {
                        None
                    }


@@ 601,31 614,22 @@ pub fn spawn_enqueue_send_community_follow(
            }
        };

        let mut follow = activitystreams::activity::Follow::new();
        follow
            .object_props
            .set_context_xsd_any_uri(activitystreams::context())?;
        follow
            .object_props
            .set_id(get_local_community_follow_apub_id(
                community,
                local_follower,
                &ctx.host_url_apub,
            ))?;

        let person_ap_id = get_local_person_apub_id(local_follower, &ctx.host_url_apub);

        follow.follow_props.set_actor_xsd_any_uri(person_ap_id)?;

        let mut follow =
            activitystreams::activity::Follow::new(person_ap_id, community_ap_id.clone());
        follow
            .follow_props
            .set_object_xsd_any_uri(community_ap_id.as_ref())?;
        follow.object_props.set_to_xsd_any_uri(community_ap_id)?;
            .set_context(activitystreams::context())
            .set_id(
                get_local_community_follow_apub_id(community, local_follower, &ctx.host_url_apub)
                    .into(),
            )
            .set_to(community_ap_id);

        std::mem::drop(db);

        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
            inbox: (&community_inbox).into(),
            inbox: Cow::Owned(community_inbox),
            sign_as: Some(crate::ActorLocalRef::Person(local_follower)),
            object: serde_json::to_string(&follow)?,
        })


@@ 642,12 646,12 @@ pub fn spawn_enqueue_send_community_follow_undo(
    ctx: Arc<crate::RouteContext>,
) {
    crate::spawn_task(async move {
        let (_community_ap_id, community_inbox): (String, String) = {
        let community_inbox: url::Url = {
            let db = ctx.db_pool.get().await?;

            let row = db
                .query_one(
                    "SELECT local, ap_id, ap_inbox FROM community WHERE id=$1",
                    "SELECT local, ap_inbox FROM community WHERE id=$1",
                    &[&community_local_id],
                )
                .await?;


@@ 656,24 660,16 @@ pub fn spawn_enqueue_send_community_follow_undo(
                // no need to send follow state to ourself
                return Ok(());
            } else {
                let ap_id = row.get(1);
                let ap_inbox = row.get(2);
                let ap_inbox: Option<&str> = row.get(1);

                (if let Some(ap_id) = ap_id {
                    if let Some(ap_inbox) = ap_inbox {
                        Some((ap_id, ap_inbox))
                    } else {
                        None
                    }
                } else {
                    None
                })
                .ok_or_else(|| {
                    crate::Error::InternalStr(format!(
                        "Missing apub info for community {}",
                        community_local_id,
                    ))
                })?
                ap_inbox
                    .ok_or_else(|| {
                        crate::Error::InternalStr(format!(
                            "Missing apub info for community {}",
                            community_local_id,
                        ))
                    })?
                    .parse()?
            }
        };



@@ 685,7 681,7 @@ pub fn spawn_enqueue_send_community_follow_undo(
        )?;

        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
            inbox: (&community_inbox).into(),
            inbox: Cow::Owned(community_inbox),
            sign_as: Some(crate::ActorLocalRef::Person(local_follower)),
            object: serde_json::to_string(&undo)?,
        })


@@ 698,31 694,28 @@ pub fn spawn_enqueue_send_community_follow_undo(
pub fn local_community_post_announce_ap(
    community_id: CommunityLocalID,
    post_local_id: PostLocalID,
    post_ap_id: &str,
    host_url_apub: &str,
    post_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Announce, crate::Error> {
    let mut announce = activitystreams::activity::Announce::new();

    let community_ap_id = get_local_community_apub_id(community_id, host_url_apub);

    announce
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    announce.object_props.set_id(format!(
        "{}/posts/{}/announce",
        community_ap_id, post_local_id,
    ))?;
    announce
        .object_props
        .set_to_xsd_any_uri(format!("{}/followers", community_ap_id))?;
    announce
        .object_props
        .set_cc_xsd_any_uri(activitystreams::public())?;
    let mut announce =
        activitystreams::activity::Announce::new(community_ap_id.clone(), post_ap_id);

    announce
        .announce_props
        .set_actor_xsd_any_uri(community_ap_id)?;
    announce.announce_props.set_object_xsd_any_uri(post_ap_id)?;
        .set_context(activitystreams::context())
        .set_id({
            let mut res = community_ap_id.clone();
            res.path_segments_mut()
                .extend(&["posts", &post_local_id.to_string(), "announce"]);
            res.into()
        })
        .set_to({
            let mut res = community_ap_id;
            res.path_segments_mut().push("followers");
            BaseURL::from(res)
        })
        .set_cc(activitystreams::public());

    Ok(announce)
}


@@ 730,27 723,20 @@ pub fn local_community_post_announce_ap(
pub fn local_community_comment_announce_ap(
    community_id: CommunityLocalID,
    comment_local_id: CommentLocalID,
    comment_ap_id: &str,
    host_url_apub: &str,
    comment_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Announce, crate::Error> {
    let mut announce = activitystreams::activity::Announce::new();

    let community_ap_id = get_local_community_apub_id(community_id, host_url_apub);

    announce
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    announce.object_props.set_id(format!(
        "{}/comments/{}/announce",
        community_ap_id, comment_local_id,
    ))?;
    let mut announce =
        activitystreams::activity::Announce::new(community_ap_id.deref().clone(), comment_ap_id);

    announce
        .announce_props
        .set_actor_xsd_any_uri(community_ap_id)?;
    announce
        .announce_props
        .set_object_xsd_any_uri(comment_ap_id)?;
    announce.set_context(activitystreams::context()).set_id({
        let mut res = community_ap_id;
        res.path_segments_mut()
            .extend(&["comments", &comment_local_id.to_string(), "announce"]);
        res.into()
    });

    Ok(announce)
}


@@ 758,7 744,7 @@ pub fn local_community_comment_announce_ap(
pub fn spawn_announce_community_post(
    community: CommunityLocalID,
    post_local_id: PostLocalID,
    post_ap_id: &str,
    post_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    // since post is borrowed, we can't move it


@@ 779,7 765,7 @@ pub fn spawn_announce_community_post(
pub fn spawn_announce_community_comment(
    community: CommunityLocalID,
    comment_local_id: CommentLocalID,
    comment_ap_id: &str,
    comment_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    let announce = local_community_comment_announce_ap(


@@ 798,20 784,19 @@ pub fn spawn_announce_community_comment(
pub fn local_community_update_to_ap(
    community_id: CommunityLocalID,
    update_id: uuid::Uuid,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Update, crate::Error> {
    let mut update = activitystreams::activity::Update::new();

    let community_ap_id = get_local_community_apub_id(community_id, host_url_apub);

    update
        .object_props
        .set_id(format!("{}/updates/{}", community_ap_id, update_id))?;
    let mut update =
        activitystreams::activity::Update::new(community_ap_id.clone(), community_ap_id.clone());

    update
        .update_props
        .set_actor_xsd_any_uri(community_ap_id.as_ref())?
        .set_object_xsd_any_uri(community_ap_id)?;
    update.set_id({
        let mut res = community_ap_id;
        res.path_segments_mut()
            .extend(&["updates", &update_id.to_string()]);
        res.into()
    });

    Ok(update)
}


@@ 820,44 805,35 @@ pub fn local_community_follow_undo_to_ap(
    undo_id: uuid::Uuid,
    community_local_id: CommunityLocalID,
    local_follower: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Undo, crate::Error> {
    let mut undo = activitystreams::activity::Undo::new();
    undo.object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    undo.undo_props
        .set_object_xsd_any_uri(get_local_community_follow_apub_id(
            community_local_id,
            local_follower,
            &host_url_apub,
        ))?;
    undo.undo_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(local_follower, &host_url_apub))?;
    undo.object_props.set_id(format!(
        "{}/community_follow_undos/{}",
        host_url_apub, undo_id
    ))?;
    let mut undo = activitystreams::activity::Undo::new(
        get_local_person_apub_id(local_follower, &host_url_apub),
        get_local_community_follow_apub_id(community_local_id, local_follower, &host_url_apub),
    );
    undo.set_context(activitystreams::context()).set_id({
        let mut res = host_url_apub.clone();
        res.path_segments_mut()
            .extend(&["community_follow_undos", &undo_id.to_string()]);
        res.into()
    });

    Ok(undo)
}

pub fn community_follow_accept_to_ap(
    community_ap_id: &str,
    community_ap_id: BaseURL,
    follower_local_id: UserLocalID,
    follow_ap_id: &str,
    follow_ap_id: url::Url,
) -> Result<activitystreams::activity::Accept, crate::Error> {
    let mut accept = activitystreams::activity::Accept::new();
    let mut accept = activitystreams::activity::Accept::new(community_ap_id.clone(), follow_ap_id);

    accept
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    accept.object_props.set_id(format!(
        "{}/followers/{}/accept",
        community_ap_id, follower_local_id
    ))?;

    accept.accept_props.set_actor_xsd_any_uri(community_ap_id)?;
    accept.accept_props.set_object_xsd_any_uri(follow_ap_id)?;
    accept.set_context(activitystreams::context()).set_id({
        let mut res = community_ap_id;
        res.path_segments_mut()
            .extend(&["followers", &follower_local_id.to_string(), "accept"]);
        res.into()
    });

    Ok(accept)
}


@@ 871,12 847,15 @@ pub fn spawn_enqueue_send_community_follow_accept(
    crate::spawn_task(async move {
        let db = ctx.db_pool.get().await?;

        let follow_ap_id = follow
            .object_props
            .get_id()
        let follow_ap_id = {
            (match follow.into_inner() {
                Cow::Owned(follow) => follow.into_inner().take_id(),
                Cow::Borrowed(follow) => follow.id_unchecked().cloned(),
            })
            .ok_or(crate::Error::InternalStrStatic(
                "Missing ID in Follow activity",
            ))?;
            ))?
        };

        let community_ap_id = get_local_community_apub_id(local_community, &ctx.host_url_apub);



@@ 893,16 872,20 @@ pub fn spawn_enqueue_send_community_follow_accept(
                // Shouldn't happen, but fine to ignore it
                return Ok(());
            } else {
                let ap_inbox: Option<String> = row.get(1);

                ap_inbox.ok_or_else(|| {
                    crate::Error::InternalStr(format!("Missing apub info for user {}", follower))
                })?
                let ap_inbox: Option<&str> = row.get(1);

                ap_inbox
                    .ok_or_else(|| {
                        crate::Error::InternalStr(format!(
                            "Missing apub info for user {}",
                            follower
                        ))
                    })?
                    .parse()?
            }
        };

        let accept =
            community_follow_accept_to_ap(&community_ap_id, follower, follow_ap_id.as_str())?;
        let accept = community_follow_accept_to_ap(community_ap_id, follower, follow_ap_id)?;
        println!("{:?}", accept);

        let body = serde_json::to_string(&accept)?;


@@ 910,7 893,7 @@ pub fn spawn_enqueue_send_community_follow_accept(
        std::mem::drop(db);

        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
            inbox: (&follower_inbox).into(),
            inbox: Cow::Owned(follower_inbox),
            sign_as: Some(crate::ActorLocalRef::Community(local_community)),
            object: body,
        })


@@ 922,23 905,21 @@ pub fn spawn_enqueue_send_community_follow_accept(

pub fn post_to_ap(
    post: &crate::PostInfo<'_>,
    community_ap_id: &str,
    host_url_apub: &str,
) -> Result<activitystreams::BaseBox, crate::Error> {
    use std::convert::TryInto;

    let apply_content = |props: &mut activitystreams::object::properties::ObjectProperties| -> Result<(), crate::Error> {
    community_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::base::AnyBase, crate::Error> {
    fn apply_content<
        K,
        O: activitystreams::object::ObjectExt<K> + activitystreams::base::BaseExt<K>,
    >(
        props: &mut O,
        post: &crate::PostInfo,
    ) {
        if let Some(html) = post.content_html {
            props
                .set_content_xsd_string(html)?
                .set_media_type(mime::TEXT_HTML)?;
            props.set_content(html).set_media_type(mime::TEXT_HTML);
        } else if let Some(text) = post.content_text {
            props
                .set_content_xsd_string(text)?
                .set_media_type(mime::TEXT_PLAIN)?;
            props.set_content(text).set_media_type(mime::TEXT_PLAIN);
        }

        Ok(())
    };

    match post.href {


@@ 946,115 927,97 @@ pub fn post_to_ap(
            let mut post_ap = activitystreams::object::Page::new();

            post_ap
                .as_mut()
                .set_context_xsd_any_uri(activitystreams::context())?
                .set_id(get_local_post_apub_id(post.id, &host_url_apub))?
                .set_attributed_to_xsd_any_uri(get_local_person_apub_id(
                .set_context(activitystreams::context())
                .set_id(get_local_post_apub_id(post.id, &host_url_apub).into())
                .set_attributed_to(get_local_person_apub_id(
                    post.author.unwrap(),
                    &host_url_apub,
                ))?
                .set_url_xsd_any_uri(href)?
                .set_summary_xsd_string(post.title)?
                .set_published(*post.created)?
                .set_to_xsd_any_uri(community_ap_id)?
                .set_cc_xsd_any_uri(activitystreams::public())?;
                ))
                .set_url(href.to_owned())
                .set_summary(post.title)
                .set_published(*post.created)
                .set_to(community_ap_id)
                .set_cc(activitystreams::public());

            apply_content(post_ap.as_mut())?;
            apply_content(&mut post_ap, post);

            Ok(post_ap.try_into()?)
            Ok(post_ap.into_any_base()?)
        }
        None => {
            let mut post_ap = activitystreams::object::Note::new();

            post_ap
                .as_mut()
                .set_context_xsd_any_uri(activitystreams::context())?
                .set_id(get_local_post_apub_id(post.id, &host_url_apub))?
                .set_attributed_to_xsd_any_uri(get_local_person_apub_id(
                .set_context(activitystreams::context())
                .set_id(get_local_post_apub_id(post.id, &host_url_apub).into())
                .set_attributed_to(Into::<url::Url>::into(get_local_person_apub_id(
                    post.author.unwrap(),
                    &host_url_apub,
                ))?
                .set_summary_xsd_string(post.title)?
                .set_published(*post.created)?
                .set_to_xsd_any_uri(community_ap_id)?
                .set_cc_xsd_any_uri(activitystreams::public())?;
                )))
                .set_summary(post.title)
                .set_published(*post.created)
                .set_to(community_ap_id)
                .set_cc(activitystreams::public());

            apply_content(post_ap.as_mut())?;
            apply_content(&mut post_ap, post);

            Ok(post_ap.try_into()?)
            Ok(post_ap.into_any_base()?)
        }
    }
}

pub fn local_post_to_create_ap(
    post: &crate::PostInfo<'_>,
    community_ap_id: &str,
    host_url_apub: &str,
    community_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Create, crate::Error> {
    let post_ap = post_to_ap(&post, &community_ap_id, &host_url_apub)?;

    let mut create = activitystreams::activity::Create::new();
    create
        .create_props
        .set_object_base_box(post_ap)?
        .set_actor_xsd_any_uri(get_local_person_apub_id(
            post.author.unwrap(),
            &host_url_apub,
        ))?;
    create
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?
        .set_id(format!(
            "{}/create",
            get_local_post_apub_id(post.id, host_url_apub)
        ))?;
    let post_ap = post_to_ap(&post, community_ap_id, &host_url_apub)?;

    let mut create = activitystreams::activity::Create::new(
        get_local_person_apub_id(post.author.unwrap(), &host_url_apub),
        post_ap,
    );
    create.set_context(activitystreams::context()).set_id({
        let mut res = get_local_post_apub_id(post.id, host_url_apub);
        res.path_segments_mut().push("create");
        res.into()
    });

    Ok(create)
}

pub fn local_comment_to_ap(
    comment: &crate::CommentInfo,
    post_ap_id: &str,
    parent_ap_id: Option<&str>,
    parent_or_post_author_ap_id: Option<&str>,
    community_ap_id: &str,
    host_url_apub: &str,
    post_ap_id: &url::Url,
    parent_ap_id: Option<url::Url>,
    parent_or_post_author_ap_id: Option<url::Url>,
    community_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::object::Note, crate::Error> {
    let mut obj = activitystreams::object::Note::new();

    obj.as_mut()
        .set_context_xsd_any_uri(activitystreams::context())?
        .set_id(get_local_comment_apub_id(comment.id, &host_url_apub))?
        .set_attributed_to_xsd_any_uri(get_local_person_apub_id(
    obj.set_context(activitystreams::context())
        .set_id(get_local_comment_apub_id(comment.id, &host_url_apub).into())
        .set_attributed_to(url::Url::from(get_local_person_apub_id(
            comment.author.unwrap(),
            &host_url_apub,
        ))?
        .set_published(comment.created)?
        .set_in_reply_to_xsd_any_uri(parent_ap_id.unwrap_or(post_ap_id))?;
        )))
        .set_published(comment.created)
        .set_in_reply_to(parent_ap_id.unwrap_or_else(|| post_ap_id.clone()));

    if let Some(html) = &comment.content_html {
        obj.as_mut()
            .set_content_xsd_string(html.as_ref())?
            .set_media_type(mime::TEXT_HTML)?;
        obj.set_content(html.as_ref().to_owned())
            .set_media_type(mime::TEXT_HTML);
    } else if let Some(text) = &comment.content_text {
        obj.as_mut()
            .set_content_xsd_string(text.as_ref())?
            .set_media_type(mime::TEXT_PLAIN)?;
        obj.set_content(text.as_ref().to_owned())
            .set_media_type(mime::TEXT_PLAIN);
    }

    if let Some(parent_or_post_author_ap_id) = parent_or_post_author_ap_id {
        use std::convert::TryInto;

        obj.as_mut()
            .set_to_xsd_any_uri(parent_or_post_author_ap_id)?
            .set_many_cc_xsd_any_uris(vec![
                activitystreams::public(),
                community_ap_id.try_into()?,
            ])?;
        obj.set_to(parent_or_post_author_ap_id)
            .set_many_ccs(vec![activitystreams::public(), community_ap_id]);
    } else {
        obj.as_mut()
            .set_to_xsd_any_uri(community_ap_id)?
            .set_cc_xsd_any_uri(activitystreams::public())?;
        obj.set_to(community_ap_id)
            .set_cc(activitystreams::public());
    }

    Ok(obj)


@@ 1067,7 1030,7 @@ pub fn spawn_enqueue_send_local_post_to_community(
    crate::spawn_task(async move {
        let db = ctx.db_pool.get().await?;

        let (community_ap_id, community_inbox): (String, String) = {
        let (community_ap_id, community_inbox): (url::Url, url::Url) = {
            let row = db
                .query_one(
                    "SELECT local, ap_id, COALESCE(ap_shared_inbox, ap_inbox) FROM community WHERE id=$1",


@@ 1079,12 1042,12 @@ pub fn spawn_enqueue_send_local_post_to_community(
                // no need to send posts for local communities
                return Ok(());
            } else {
                let ap_id = row.get(1);
                let ap_inbox = row.get(2);
                let ap_id: Option<&str> = row.get(1);
                let ap_inbox: Option<&str> = row.get(2);

                (if let Some(ap_id) = ap_id {
                    if let Some(ap_inbox) = ap_inbox {
                        Some((ap_id, ap_inbox))
                        Some((ap_id.parse()?, ap_inbox.parse()?))
                    } else {
                        None
                    }


@@ 1100,11 1063,10 @@ pub fn spawn_enqueue_send_local_post_to_community(
            }
        };

        let create =
            local_post_to_create_ap(&(&post).into(), &community_ap_id, &ctx.host_url_apub)?;
        let create = local_post_to_create_ap(&(&post).into(), community_ap_id, &ctx.host_url_apub)?;

        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
            inbox: (&community_inbox).into(),
            inbox: Cow::Owned(community_inbox),
            sign_as: Some(crate::ActorLocalRef::Person(post.author.unwrap())),
            object: serde_json::to_string(&create)?,
        })


@@ 1117,18 1079,18 @@ pub fn spawn_enqueue_send_local_post_to_community(
pub fn local_post_delete_to_ap(
    post_id: PostLocalID,
    author: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Delete, crate::Error> {
    let mut delete = activitystreams::activity::Delete::new();
    let post_ap_id = get_local_post_apub_id(post_id, host_url_apub);
    delete
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?
        .set_id(format!("{}/delete", post_ap_id))?;
    delete
        .delete_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(author, host_url_apub))?;
    delete.delete_props.set_object_xsd_any_uri(post_ap_id)?;
    let mut delete = activitystreams::activity::Delete::new(
        get_local_person_apub_id(author, host_url_apub),
        post_ap_id.clone(),
    );
    delete.set_context(activitystreams::context()).set_id({
        let mut res = post_ap_id;
        res.path_segments_mut().push("delete");
        res.into()
    });

    Ok(delete)
}


@@ 1136,77 1098,68 @@ pub fn local_post_delete_to_ap(
pub fn local_comment_delete_to_ap(
    comment_id: CommentLocalID,
    author: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Delete, crate::Error> {
    let mut delete = activitystreams::activity::Delete::new();
    let comment_ap_id = get_local_comment_apub_id(comment_id, host_url_apub);
    delete
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?
        .set_id(format!("{}/delete", comment_ap_id))?;
    delete
        .delete_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(author, host_url_apub))?;
    delete.delete_props.set_object_xsd_any_uri(comment_ap_id)?;

    let mut delete = activitystreams::activity::Delete::new(
        get_local_person_apub_id(author, host_url_apub),
        comment_ap_id.clone(),
    );

    delete.set_context(activitystreams::context()).set_id({
        let mut res = comment_ap_id;
        res.path_segments_mut().push("delete");
        res.into()
    });

    Ok(delete)
}

pub fn local_comment_to_create_ap(
    comment: &crate::CommentInfo,
    post_ap_id: &str,
    parent_ap_id: Option<&str>,
    post_or_parent_author_ap_id: Option<&str>,
    community_ap_id: &str,
    host_url_apub: &str,
    post_ap_id: &url::Url,
    parent_ap_id: Option<url::Url>,
    post_or_parent_author_ap_id: Option<url::Url>,
    community_ap_id: url::Url,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Create, crate::Error> {
    let comment_ap = local_comment_to_ap(
        &comment,
        &post_ap_id,
        post_ap_id,
        parent_ap_id,
        post_or_parent_author_ap_id,
        &community_ap_id,
        community_ap_id,
        &host_url_apub,
    )?;

    let author = comment.author.unwrap();

    let mut create = activitystreams::activity::Create::new();
    create
        .object_props
        .set_context_xsd_any_uri(activitystreams::context())?
        .set_id(format!(
            "{}/create",
            get_local_comment_apub_id(comment.id, host_url_apub)
        ))?;
    create.create_props.set_object_base_box(comment_ap)?;
    create
        .create_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(author, &host_url_apub))?;
    let mut create = activitystreams::activity::Create::new(
        get_local_person_apub_id(author, host_url_apub),
        comment_ap.into_any_base()?,
    );
    create.set_context(activitystreams::context()).set_id({
        let mut res = get_local_comment_apub_id(comment.id, host_url_apub);
        res.path_segments_mut().push("create");
        res.into()
    });

    Ok(create)
}

pub fn local_post_like_to_ap(
    post_local_id: PostLocalID,
    post_ap_id: &str,
    post_ap_id: BaseURL,
    user: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Like, crate::Error> {
    let mut like = activitystreams::activity::Like::new();
    like.like_props.set_object_xsd_any_uri(post_ap_id)?;
    like.like_props
        .set_actor_xsd_any_uri(crate::apub_util::get_local_person_apub_id(
            user,
            &host_url_apub,
        ))?;
    like.object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    like.object_props.set_id(get_local_post_like_apub_id(
        post_local_id,
        user,
        &host_url_apub,
    ))?;
    let mut like = activitystreams::activity::Like::new(
        crate::apub_util::get_local_person_apub_id(user, &host_url_apub),
        post_ap_id,
    );
    like.set_context(activitystreams::context())
        .set_id(get_local_post_like_apub_id(post_local_id, user, &host_url_apub).into());

    Ok(like)
}


@@ 1215,43 1168,36 @@ pub fn local_post_like_undo_to_ap(
    undo_id: uuid::Uuid,
    post_local_id: PostLocalID,
    user: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Undo, crate::Error> {
    let like_ap_id = get_local_post_like_apub_id(post_local_id, user, &host_url_apub);

    let mut undo = activitystreams::activity::Undo::new();
    undo.undo_props
        .set_object_xsd_any_uri(like_ap_id.as_ref())?;
    undo.undo_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(user, &host_url_apub))?;
    undo.object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    undo.object_props
        .set_id(format!("{}/post_like_undos/{}", host_url_apub, undo_id,))?;
    let mut undo = activitystreams::activity::Undo::new(
        get_local_person_apub_id(user, &host_url_apub),
        like_ap_id,
    );
    undo.set_context(activitystreams::context()).set_id({
        let mut res = host_url_apub.clone();
        res.path_segments_mut()
            .extend(&["post_like_undos", &undo_id.to_string()]);
        res.into()
    });

    Ok(undo)
}

pub fn local_comment_like_to_ap(
    comment_local_id: CommentLocalID,
    comment_ap_id: &str,
    comment_ap_id: BaseURL,
    user: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Like, crate::Error> {
    let mut like = activitystreams::activity::Like::new();
    like.like_props.set_object_xsd_any_uri(comment_ap_id)?;
    like.like_props
        .set_actor_xsd_any_uri(crate::apub_util::get_local_person_apub_id(
            user,
            &host_url_apub,
        ))?;
    like.object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    like.object_props.set_id(get_local_comment_like_apub_id(
        comment_local_id,
        user,
        &host_url_apub,
    ))?;
    let mut like = activitystreams::activity::Like::new(
        crate::apub_util::get_local_person_apub_id(user, &host_url_apub),
        comment_ap_id,
    );
    like.set_context(activitystreams::context())
        .set_id(get_local_comment_like_apub_id(comment_local_id, user, &host_url_apub).into());

    Ok(like)
}


@@ 1260,38 1206,39 @@ pub fn local_comment_like_undo_to_ap(
    undo_id: uuid::Uuid,
    comment_local_id: CommentLocalID,
    user: UserLocalID,
    host_url_apub: &str,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Undo, crate::Error> {
    let like_ap_id = get_local_comment_like_apub_id(comment_local_id, user, &host_url_apub);

    let mut undo = activitystreams::activity::Undo::new();
    undo.undo_props
        .set_object_xsd_any_uri(like_ap_id.as_ref())?;
    undo.undo_props
        .set_actor_xsd_any_uri(get_local_person_apub_id(user, &host_url_apub))?;
    undo.object_props
        .set_context_xsd_any_uri(activitystreams::context())?;
    undo.object_props
        .set_id(format!("{}/comment_like_undos/{}", host_url_apub, undo_id,))?;
    let mut undo = activitystreams::activity::Undo::new(
        get_local_person_apub_id(user, &host_url_apub),
        like_ap_id,
    );
    undo.set_context(activitystreams::context()).set_id({
        let mut res = host_url_apub.clone();
        res.path_segments_mut()
            .extend(&["comment_like_undos", &undo_id.to_string()]);
        res.into()
    });

    Ok(undo)
}

pub fn spawn_enqueue_send_comment_to_community(
    comment: crate::CommentInfo,
    community_ap_id: &str,
    community_ap_inbox: String,
    post_ap_id: String,
    parent_ap_id: Option<String>,
    post_or_parent_author_ap_id: Option<&str>,
    community_ap_id: url::Url,
    community_ap_inbox: url::Url,
    post_ap_id: url::Url,
    parent_ap_id: Option<url::Url>,
    post_or_parent_author_ap_id: Option<url::Url>,
    ctx: Arc<crate::RouteContext>,
) {
    let create = local_comment_to_create_ap(
        &comment,
        &post_ap_id,
        parent_ap_id.as_deref(),
        parent_ap_id,
        post_or_parent_author_ap_id,
        &community_ap_id,
        community_ap_id,
        &ctx.host_url_apub,
    );



@@ 1301,7 1248,7 @@ pub fn spawn_enqueue_send_comment_to_community(
        let create = create?;

        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
            inbox: community_ap_inbox.into(),
            inbox: Cow::Owned(community_ap_inbox),
            sign_as: Some(crate::ActorLocalRef::Person(author)),
            object: serde_json::to_string(&create)?,
        })


@@ 1338,11 1285,11 @@ async fn enqueue_send_to_community_followers(
}

pub fn maybe_get_local_community_id_from_uri(
    uri: &str,
    host_url_apub: &str,
    uri: &url::Url,
    host_url_apub: &BaseURL,
) -> Option<CommunityLocalID> {
    if uri.starts_with(&host_url_apub) {
        let path = &uri[host_url_apub.len()..];
    if uri.as_str().starts_with(&host_url_apub.as_str()) {
        let path = &uri.as_str()[host_url_apub.as_str().len()..];
        if path.starts_with("/communities/") {
            if let Ok(local_community_id) = path[13..].parse() {
                Some(local_community_id)


@@ 1357,52 1304,23 @@ pub fn maybe_get_local_community_id_from_uri(
    }
}

pub async fn handle_recieved_object_for_local_community(
    obj: Verified<activitystreams::object::ObjectBox>,
pub async fn handle_recieved_object_for_local_community<'a>(
    obj: Verified<KnownObject>,
    ctx: Arc<crate::RouteContext>,
) -> Result<(), crate::Error> {
    let (to, in_reply_to, obj_id, obj) = match obj.kind() {
        Some("Page") => {
            let obj = obj
                .into_concrete_object::<activitystreams::object::Page>()
                .unwrap();
            (
                obj.object_props.to.clone(),
                None,
                obj.object_props.id.clone(),
                obj.try_box_object().unwrap(),
            )
        }
        Some("Note") => {
            let obj = obj
                .into_concrete_object::<activitystreams::object::Note>()
                .unwrap();
            (
                obj.object_props.to.clone(),
                obj.object_props.in_reply_to.clone(),
                obj.object_props.id.clone(),
                obj.try_box_object().unwrap(),
            )
        }
        _ => (None, None, None, obj),
    let (to, in_reply_to, obj_id) = match obj.deref() {
        KnownObject::Page(obj) => (obj.to(), None, obj.id_unchecked()),
        KnownObject::Note(obj) => (obj.to(), obj.in_reply_to(), obj.id_unchecked()),
        _ => (None, None, None),
    };

    let local_community_id = match to {
        None => None,
        Some(activitystreams::object::properties::ObjectPropertiesToEnum::Term(term)) => match term
        {
            activitystreams::object::properties::ObjectPropertiesToTermEnum::XsdAnyUri(uri) => {
                maybe_get_local_community_id_from_uri(uri.as_str(), &ctx.host_url_apub)
            }
            _ => None,
        },
        Some(activitystreams::object::properties::ObjectPropertiesToEnum::Array(terms)) => terms
        Some(maybe) => maybe
            .iter()
            .filter_map(|term| match term {
                activitystreams::object::properties::ObjectPropertiesToTermEnum::XsdAnyUri(uri) => {
                    maybe_get_local_community_id_from_uri(uri.as_str(), &ctx.host_url_apub)
                }
                _ => None,
            .filter_map(|any| {
                any.as_xsd_any_uri()
                    .and_then(|uri| maybe_get_local_community_id_from_uri(uri, &ctx.host_url_apub))
            })
            .next(),
    };


@@ 1413,25 1331,23 @@ pub async fn handle_recieved_object_for_local_community(
        // not to a community, but might still match as a reply
        if let Some(in_reply_to) = in_reply_to {
            if let Some(obj_id) = obj_id {
                if let Ok(obj) = obj.into_concrete_object::<activitystreams::object::Note>() {
                if let KnownObject::Note(obj) = obj.deref() {
                    // TODO deduplicate this?

                    let content = obj.as_ref().get_content_xsd_string().map(|x| x.as_str());
                    let media_type = obj.as_ref().get_media_type().map(|x| x.as_ref());
                    let created = obj.as_ref().get_published().map(|x| x.as_datetime());
                    let author = obj.as_ref().get_attributed_to_xsd_any_uri();
                    let content = obj.content().and_then(|x| x.as_single_xsd_string());
                    let media_type = obj.media_type();
                    let created = obj.published();
                    let author = obj.attributed_to().and_then(|x| x.as_single_id());

                    if let Some(author) = author {
                        require_containment(obj_id.as_url(), author.as_url())?;
                        require_containment(obj_id, author)?;
                    }

                    let author = author.map(|x| x.as_str());

                    handle_recieved_reply(
                        obj_id.as_ref(),
                        obj_id.try_into()?,
                        content.unwrap_or(""),
                        media_type,
                        created,
                        created.as_ref(),
                        author,
                        &in_reply_to,
                        ctx,


@@ 1445,42 1361,53 @@ pub async fn handle_recieved_object_for_local_community(
    Ok(())
}

pub async fn handle_recieved_object_for_community(
pub async fn handle_recieved_object_for_community<'a>(
    community_local_id: CommunityLocalID,
    community_is_local: bool,
    obj: Verified<activitystreams::object::ObjectBox>,
    obj: Verified<KnownObject>,
    ctx: Arc<crate::RouteContext>,
) -> Result<(), crate::Error> {
    println!("recieved object: {:?}", obj);

    match obj.kind() {
        Some("Page") => {
            let obj: Verified<activitystreams::object::Page> = obj.into_concrete_object().unwrap();
    match obj.into_inner() {
        KnownObject::Page(obj) => {
            let title = obj
                .as_ref()
                .get_summary_xsd_string()
                .map(|x| x.as_str())
                .summary()
                .iter()
                .map(|x| x.iter())
                .flatten()
                .filter_map(|maybe| maybe.as_xsd_string())
                .next()
                .unwrap_or("");
            let href = obj.as_ref().get_url_xsd_any_uri().map(|x| x.as_str());
            let content = obj.as_ref().get_content_xsd_string().map(|x| x.as_str());
            let media_type = obj.as_ref().get_media_type().map(|x| x.as_ref());
            let created = obj.as_ref().get_published().map(|x| x.as_datetime());
            // TODO support objects here?
            let author = obj.as_ref().get_attributed_to_xsd_any_uri();
            if let Some(object_id) = &obj.as_ref().id {
            let href = obj
                .url()
                .iter()
                .map(|x| x.iter())
                .flatten()
                .filter_map(|maybe| {
                    maybe
                        .as_xsd_any_uri()
                        .map(|x| x.as_str())
                        .or_else(|| maybe.as_xsd_string())
                })
                .next();
            let content = obj.content().and_then(|x| x.as_single_xsd_string());
            let media_type = obj.media_type();
            let created = obj.published();
            let author = obj.attributed_to().and_then(|x| x.as_single_id());

            if let Some(object_id) = obj.id_unchecked() {
                if let Some(author) = author {
                    require_containment(object_id.as_url(), author.as_url())?;
                    require_containment(&object_id, author)?;
                }

                let author = author.map(|x| x.as_str());

                handle_recieved_post(
                    object_id.as_str(),
                    object_id.clone(),
                    title,
                    href,
                    content,
                    media_type,
                    created,
                    created.as_ref(),
                    author,
                    community_local_id,
                    community_is_local,


@@ 1489,25 1416,21 @@ pub async fn handle_recieved_object_for_community(
                .await?;
            }
        }
        Some("Note") => {
            let obj: Verified<activitystreams::object::Note> = obj.into_concrete_object().unwrap();
            let content = obj.as_ref().get_content_xsd_string().map(|x| x.as_str());
            let media_type = obj.as_ref().get_media_type().map(|x| x.as_ref());
            let created = obj.as_ref().get_published().map(|x| x.as_datetime());
            let author = obj
                .as_ref()
                .get_attributed_to_xsd_any_uri()
                .map(|x| x.as_str());

            if let Some(object_id) = &obj.as_ref().id {
                if let Some(in_reply_to) = &obj.as_ref().in_reply_to {
        KnownObject::Note(obj) => {
            let content = obj.content().and_then(|x| x.as_single_xsd_string());
            let media_type = obj.media_type();
            let created = obj.published();
            let author = obj.attributed_to().and_then(|x| x.as_single_id());

            if let Some(object_id) = obj.id_unchecked() {
                if let Some(in_reply_to) = obj.in_reply_to() {
                    // it's a reply

                    handle_recieved_reply(
                        object_id.as_ref(),
                        object_id,
                        content.unwrap_or(""),
                        media_type,
                        created,
                        created.as_ref(),
                        author,
                        in_reply_to,
                        ctx,


@@ 1516,45 1439,42 @@ pub async fn handle_recieved_object_for_community(
                } else {
                    // not a reply, must be a top-level post
                    let title = obj
                        .as_ref()
                        .get_summary_xsd_string()
                        .map(|x| x.as_str())
                        .summary()
                        .and_then(|x| x.as_single_xsd_string())
                        .unwrap_or("");

                    // Interpret attachments (usually images) as links
                    let href = (match &obj.as_ref().attachment {
                        Some(activitystreams::object::properties::ObjectPropertiesAttachmentEnum::Array(vec)) => vec.iter().next(),
                        Some(activitystreams::object::properties::ObjectPropertiesAttachmentEnum::Term(term)) => Some(term),
                        None => None,
                    })
                        .and_then(|term| {
                            match term {
                                activitystreams::object::properties::ObjectPropertiesAttachmentTermEnum::BaseBox(base) => Some(base),
                                _ => None,
                            }
                        })
                        .map(|base| -> Result<_, crate::Error> {
                            Ok(match base.kind() {
                                Some("Document") => Some(base.clone().into_concrete::<activitystreams::object::Document>()?.object_props),
                                Some("Image") => Some(base.clone().into_concrete::<activitystreams::object::Image>()?.object_props),
                    let href = obj
                        .attachment()
                        .and_then(|x| x.iter().next())
                        .and_then(
                            |base: &activitystreams::base::AnyBase| match base.kind_str() {
                                Some("Document") => Some(
                                    activitystreams::object::Document::from_any_base(base.clone())
                                        .map(|obj| obj.unwrap().take_url()),
                                ),
                                Some("Image") => Some(
                                    activitystreams::object::Image::from_any_base(base.clone())
                                        .map(|obj| obj.unwrap().take_url()),
                                ),
                                _ => None,
                            })
                        })
                            },
                        )
                        .transpose()?
                        .flatten();
                    let href = href
                        .as_ref()
                        .and_then(|href| href.get_url_xsd_any_uri())
                        .and_then(|href| href.iter().filter_map(|x| x.as_xsd_any_uri()).next())
                        .map(|href| href.as_str());

                    if let Some(object_id) = &obj.as_ref().id {
                    if let Some(object_id) = obj.id_unchecked() {
                        handle_recieved_post(
                            object_id.as_str(),
                            object_id.clone(),
                            title,
                            href,
                            content,
                            media_type,
                            created,
                            created.as_ref(),
                            author,
                            community_local_id,
                            community_is_local,


@@ 1572,13 1492,13 @@ pub async fn handle_recieved_object_for_community(
}

async fn handle_recieved_post(
    object_id: &str,
    object_id: url::Url,
    title: &str,
    href: Option<&str>,
    content: Option<&str>,
    media_type: Option<&mime::Mime>,
    created: Option<&chrono::DateTime<chrono::FixedOffset>>,
    author: Option<&str>,
    author: Option<&url::Url>,
    community_local_id: CommunityLocalID,
    community_is_local: bool,
    ctx: Arc<crate::RouteContext>,


@@ 1600,7 1520,7 @@ async fn handle_recieved_post(

    let row = db.query_opt(
        "INSERT INTO post (author, href, content_text, content_html, title, created, community, local, ap_id) VALUES ($1, $2, $3, $4, $5, COALESCE($6, current_timestamp), $7, FALSE, $8) ON CONFLICT (ap_id) DO NOTHING RETURNING id",
        &[&author, &href, &content_text, &content_html, &title, &created, &community_local_id, &object_id],
        &[&author, &href, &content_text, &content_html, &title, &created, &community_local_id, &object_id.as_str()],
    ).await?;

    if community_is_local {


@@ 1614,12 1534,12 @@ async fn handle_recieved_post(
}

async fn handle_recieved_reply(
    object_id: &str,
    object_id: &url::Url,
    content: &str,
    media_type: Option<&mime::Mime>,
    created: Option<&chrono::DateTime<chrono::FixedOffset>>,
    author: Option<&str>,
    in_reply_to: &activitystreams::object::properties::ObjectPropertiesInReplyToEnum,
    author: Option<&url::Url>,
    in_reply_to: &activitystreams::primitives::OneOrMany<activitystreams::base::AnyBase>,
    ctx: Arc<crate::RouteContext>,
) -> Result<(), crate::Error> {
    let db = ctx.db_pool.get().await?;


@@ 1631,22 1551,10 @@ async fn handle_recieved_reply(
        None => None,
    };

    let in_reply_to = match in_reply_to {
        activitystreams::object::properties::ObjectPropertiesInReplyToEnum::Term(term) => {
            either::Either::Left(std::iter::once(term))
        }
        activitystreams::object::properties::ObjectPropertiesInReplyToEnum::Array(terms) => {
            either::Either::Right(terms.iter())
        }
    };

    let last_reply_to = in_reply_to.last(); // TODO maybe not this? Not sure how to interpret inReplyTo
    let last_reply_to = in_reply_to.iter().last(); // TODO maybe not this? Not sure how to interpret inReplyTo

    if let Some(last_reply_to) = last_reply_to {
        if let activitystreams::object::properties::ObjectPropertiesInReplyToTermEnum::XsdAnyUri(
            term_ap_id,
        ) = last_reply_to
        {
        if let Some(term_ap_id) = last_reply_to.as_xsd_any_uri() {
            #[derive(Debug)]
            enum ReplyTarget {
                Post {


@@ 1658,9 1566,8 @@ async fn handle_recieved_reply(
                },
            }

            let term_ap_id = term_ap_id.as_str();
            let target = if term_ap_id.starts_with(&ctx.host_url_apub) {
                let remaining = &term_ap_id[ctx.host_url_apub.len()..];
            let target = if term_ap_id.as_str().starts_with(&ctx.host_url_apub.as_str()) {
                let remaining = &term_ap_id.as_str()[ctx.host_url_apub.as_str().len()..];
                if remaining.starts_with("/posts/") {
                    if let Ok(local_post_id) = remaining[7..].parse() {
                        Some(ReplyTarget::Post { id: local_post_id })


@@ 1688,7 1595,7 @@ async fn handle_recieved_reply(
                }
            } else {
                let row = db
                    .query_opt("(SELECT id, post FROM reply WHERE ap_id=$1) UNION (SELECT NULL, id FROM post WHERE ap_id=$1) LIMIT 1", &[&term_ap_id])
                    .query_opt("(SELECT id, post FROM reply WHERE ap_id=$1) UNION (SELECT NULL, id FROM post WHERE ap_id=$1) LIMIT 1", &[&term_ap_id.as_str()])
                    .await?;
                row.map(|row| match row.get::<_, Option<_>>(0).map(CommentLocalID) {
                    Some(reply_id) => ReplyTarget::Comment {


@@ 1716,7 1623,7 @@ async fn handle_recieved_reply(

                let row = db.query_opt(
                    "INSERT INTO reply (post, parent, author, content_text, content_html, created, local, ap_id) VALUES ($1, $2, $3, $4, $5, COALESCE($6, current_timestamp), FALSE, $7) ON CONFLICT (ap_id) DO NOTHING RETURNING id",
                    &[&post, &parent, &author, &content_text, &content_html, &created, &object_id],
                    &[&post, &parent, &author, &content_text, &content_html, &created, &object_id.as_str()],
                    ).await?;

                if let Some(row) = row {


@@ 1751,25 1658,18 @@ pub async fn handle_like(
    let db = ctx.db_pool.get().await?;

    let activity_id = activity
        .object_props
        .get_id()
        .id_unchecked()
        .ok_or(crate::Error::InternalStrStatic("Missing activity ID"))?;

    if let Some(actor_id) = activity.like_props.get_actor_xsd_any_uri() {
        require_containment(activity_id.as_url(), actor_id.as_url())?;
    if let Some(actor_id) = activity.actor_unchecked().as_single_id() {
        require_containment(activity_id, actor_id)?;

        let actor_local_id = get_or_fetch_user_local_id(
            actor_id.as_str(),
            &db,
            &ctx.host_url_apub,
            &ctx.http_client,
        )
        .await?;
        let actor_local_id =
            get_or_fetch_user_local_id(actor_id, &db, &ctx.host_url_apub, &ctx.http_client).await?;

        if let Some(object_id) = activity.like_props.get_object_xsd_any_uri() {
            let object_id = object_id.as_str();
            let thing_local_ref = if object_id.starts_with(&ctx.host_url_apub) {
                let remaining = &object_id[ctx.host_url_apub.len()..];
        if let Some(object_id) = activity.object().as_single_id() {
            let thing_local_ref = if object_id.as_str().starts_with(&ctx.host_url_apub.as_str()) {
                let remaining = &object_id.as_str()[ctx.host_url_apub.as_str().len()..];
                if remaining.starts_with("/posts/") {
                    if let Ok(local_post_id) = remaining[7..].parse() {
                        Some(ThingLocalRef::Post(local_post_id))


@@ 1788,7 1688,7 @@ pub async fn handle_like(
            } else {
                let row = db.query_opt(
                    "(SELECT TRUE, id FROM post WHERE ap_id=$1) UNION ALL (SELECT FALSE, id FROM reply WHERE ap_id=$1) LIMIT 1",
                    &[&object_id],
                    &[&object_id.as_str()],
                ).await?;

                if let Some(row) = row {


@@ 1855,8 1755,17 @@ pub async fn handle_delete(
) -> Result<(), crate::Error> {
    let db = ctx.db_pool.get().await?;

    if let Some(object_id) = activity.delete_props.get_object_xsd_any_uri() {
        // TODO verify that this actor actually did this and is allowed to
    let activity_id = activity
        .id_unchecked()
        .ok_or(crate::Error::InternalStrStatic("Missing ID for activity"))?;
    let actor_id = activity
        .actor_unchecked()
        .as_single_id()
        .ok_or(crate::Error::InternalStrStatic("Missing ID for actor"))?;

    if let Some(object_id) = activity.object().as_single_id() {
        require_containment(activity_id, actor_id)?;
        require_containment(object_id, actor_id)?;

        let row = db.query_opt(
            "WITH deleted_post AS (UPDATE post SET href=NULL, title='[deleted]', content_text='[deleted]', content_markdown=NULL, content_html=NULL, deleted=TRUE WHERE ap_id=$1 AND deleted=FALSE RETURNING (SELECT id FROM community WHERE community.id = post.community AND community.local)), deleted_reply AS (UPDATE reply SET content_text='[deleted]', content_markdown=NULL, content_html=NULL, deleted=TRUE WHERE ap_id=$1 AND deleted=FALSE RETURNING (SELECT id FROM community WHERE community.id=(SELECT community FROM post WHERE id=reply.post) AND community.local)) (SELECT * FROM deleted_post) UNION ALL (SELECT * FROM deleted_reply) LIMIT 1",


@@ 1888,25 1797,24 @@ pub async fn handle_undo(
    ctx: Arc<crate::RouteContext>,
) -> Result<(), crate::Error> {
    let activity_id = activity
        .object_props
        .get_id()
        .id_unchecked()
        .ok_or(crate::Error::InternalStrStatic("Missing activity ID"))?;

    let actor_id =
        activity
            .undo_props
            .get_actor_xsd_any_uri()
            .actor_unchecked()
            .as_single_id()
            .ok_or(crate::Error::InternalStrStatic(
                "Missing actor for activity",
            ))?;

    let object_id = activity
        .undo_props
        .get_object_xsd_any_uri()
        .object()
        .as_single_id()
        .ok_or(crate::Error::InternalStrStatic("Missing object for Undo"))?;

    require_containment(activity_id.as_url(), actor_id.as_url())?;
    require_containment(object_id.as_url(), actor_id.as_url())?;
    require_containment(activity_id, actor_id)?;
    require_containment(object_id, actor_id)?;

    let object_id = object_id.as_str();



@@ 1935,11 1843,11 @@ pub async fn check_signature_for_actor(
    request_method: &hyper::Method,
    request_path_and_query: &str,
    headers: &hyper::header::HeaderMap,
    actor_ap_id: &str,
    actor_ap_id: &url::Url,
    db: &tokio_postgres::Client,
    http_client: &crate::HttpClient,
) -> Result<bool, crate::Error> {
    let found_key = db.query_opt("(SELECT public_key, public_key_sigalg FROM person WHERE ap_id=$1) UNION ALL (SELECT public_key, public_key_sigalg FROM community WHERE ap_id=$1) LIMIT 1", &[&actor_ap_id]).await?
    let found_key = db.query_opt("(SELECT public_key, public_key_sigalg FROM person WHERE ap_id=$1) UNION ALL (SELECT public_key, public_key_sigalg FROM community WHERE ap_id=$1) LIMIT 1", &[&actor_ap_id.as_str()]).await?
        .and_then(|row| {
            row.get::<_, Option<&[u8]>>(0).map(|key| {
                openssl::pkey::PKey::public_key_from_pem(key)


@@ 1989,43 1897,36 @@ pub async fn check_signature_for_actor(
    }
}

pub async fn verify_incoming_activity(
pub async fn verify_incoming_object(
    mut req: hyper::Request<hyper::Body>,
    db: &tokio_postgres::Client,
    http_client: &crate::HttpClient,
    apub_proxy_rewrites: bool,
) -> Result<Verified<activitystreams::activity::ActivityBox>, crate::Error> {
) -> Result<Verified<KnownObject>, crate::Error> {
    let req_body = hyper::body::to_bytes(req.body_mut()).await?;

    match req.headers().get("signature") {
        None => {
            let raw_obj_props: activitystreams::object::properties::ObjectProperties =
                serde_json::from_slice(&req_body)?;

            let ap_id = raw_obj_props.get_id().ok_or_else(|| {
            let obj: JustMaybeAPID = serde_json::from_slice(&req_body)?;
            let ap_id = obj.id.ok_or_else(|| {
                crate::Error::InternalStrStatic("Missing id in received activity")
            })?;

            let res_body = fetch_ap_object(ap_id.as_str(), http_client).await?;
            let res_body = fetch_ap_object(&ap_id, http_client).await?;

            Ok(Verified(serde_json::from_value(res_body)?))
            Ok(res_body)
        }
        Some(signature) => {
            let raw_activity_props: activitystreams::activity::properties::ActorOptOriginAndTargetProperties = serde_json::from_slice(&req_body)?;

            let actor_ap_id = match raw_activity_props.actor {
                activitystreams::activity::properties::ActorOptOriginAndTargetPropertiesActorEnum::Term(ref term) => {
                    match term {
                        activitystreams::activity::properties::ActorOptOriginAndTargetPropertiesActorTermEnum::XsdAnyUri(uri) => Cow::Borrowed(uri.as_str()),
                        activitystreams::activity::properties::ActorOptOriginAndTargetPropertiesActorTermEnum::BaseBox(raw_actor) => {
                            // TODO not this?
                            Cow::Owned(serde_json::to_value(raw_actor)?.get("id").and_then(serde_json::Value::as_str)
                                .ok_or_else(|| crate::Error::InternalStrStatic("No id found for actor"))?
                                .to_owned())
                        },
                    }
                },
                _ => return Err(crate::Error::InternalStrStatic("Found multiple actors for activity, can't verify signature"))
            let obj: JustActor = serde_json::from_slice(&req_body)?;

            let actor_ap_id = if let Some(actor) = obj.actor.as_one() {
                actor.id().ok_or(crate::Error::InternalStrStatic(
                    "No id found for actor, can't verify signature",
                ))?
            } else {
                return Err(crate::Error::InternalStrStatic(
                    "Found multiple actors for activity, can't verify signature",
                ));
            };

            let path_and_query = req

M src/main.rs => src/main.rs +131 -37
@@ 1,6 1,8 @@
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::convert::TryInto;
use std::ops::Deref;
use std::sync::Arc;
use trout::hyper::RoutingFailureExtHyper;



@@ 9,13 11,88 @@ mod routes;
mod tasks;
mod worker;

#[derive(Clone, Serialize, Deserialize)]
#[serde(try_from = "url::Url")]
#[serde(into = "url::Url")]
pub struct BaseURL(url::Url);
impl BaseURL {
    pub fn path_segments_mut(&mut self) -> url::PathSegmentsMut {
        self.0.path_segments_mut().unwrap()
    }
    pub fn set_fragment(&mut self, fragment: Option<&str>) {
        self.0.set_fragment(fragment);
    }
}

impl std::ops::Deref for BaseURL {
    type Target = url::Url;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::fmt::Display for BaseURL {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.0, f)
    }
}

#[derive(Debug)]
pub struct CannotBeABase;
impl std::fmt::Display for CannotBeABase {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "That URL cannot be a base")
    }
}

impl std::convert::TryFrom<url::Url> for BaseURL {
    type Error = CannotBeABase;

    fn try_from(src: url::Url) -> Result<BaseURL, Self::Error> {
        if src.cannot_be_a_base() {
            Err(CannotBeABase)
        } else {
            Ok(BaseURL(src))
        }
    }
}

impl std::str::FromStr for BaseURL {
    type Err = crate::Error;

    fn from_str(src: &str) -> Result<Self, Self::Err> {
        let url: url::Url = src.parse()?;

        url.try_into()
            .map_err(|_| crate::Error::InternalStrStatic("Parsed URL cannot be a base"))
    }
}

impl From<BaseURL> for url::Url {
    fn from(src: BaseURL) -> url::Url {
        src.0
    }
}

impl Into<activitystreams::base::AnyBase> for BaseURL {
    fn into(self) -> activitystreams::base::AnyBase {
        self.0.into()
    }
}

impl Into<activitystreams::primitives::OneOrMany<activitystreams::base::AnyBase>> for BaseURL {
    fn into(self) -> activitystreams::primitives::OneOrMany<activitystreams::base::AnyBase> {
        self.0.into()
    }
}

pub type DbPool = deadpool_postgres::Pool;
pub type HttpClient = hyper::Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>;

pub struct BaseContext {
    pub db_pool: DbPool,
    pub host_url_api: String,
    pub host_url_apub: String,
    pub host_url_apub: BaseURL,
    pub http_client: HttpClient,
    pub apub_proxy_rewrites: bool,



@@ 82,7 159,7 @@ impl<T: 'static + std::error::Error + Send> From<T> for Error {
#[derive(Debug, PartialEq)]
pub enum APIDOrLocal {
    Local,
    APID(String),
    APID(url::Url),
}

pub enum TimestampOrLatest {


@@ 241,15 318,17 @@ pub struct CommentInfo<'a> {

pub const KEY_BITS: u32 = 2048;

pub fn get_url_host(url: &str) -> Option<String> {
    url::Url::parse(url).ok().and_then(|url| {
        url.host_str().map(|host| match url.port() {
            Some(port) => format!("{}:{}", host, port),
            None => host.to_owned(),
        })
pub fn get_url_host(url: &url::Url) -> Option<String> {
    url.host_str().map(|host| match url.port() {
        Some(port) => format!("{}:{}", host, port),
        None => host.to_owned(),
    })
}

pub fn get_url_host_from_str(src: &str) -> Option<String> {
    src.parse().ok().as_ref().and_then(get_url_host)
}

pub fn get_actor_host<'a>(
    local: bool,
    ap_id: Option<&str>,


@@ 258,7 337,7 @@ pub fn get_actor_host<'a>(
    if local {
        Some(local_hostname.into())
    } else {
        ap_id.and_then(get_url_host).map(Cow::from)
        ap_id.and_then(get_url_host_from_str).map(Cow::from)
    }
}



@@ 270,8 349,7 @@ pub fn get_actor_host_or_unknown<'a>(
    get_actor_host(local, ap_id, local_hostname).unwrap_or(Cow::Borrowed("[unknown]"))
}

pub fn get_path_and_query(url: &str) -> Result<String, url::ParseError> {
    let url = url::Url::parse(&url)?;
pub fn get_path_and_query(url: &url::Url) -> Result<String, url::ParseError> {
    Ok(format!("{}{}", url.path(), url.query().unwrap_or("")))
}



@@ 452,7 530,7 @@ pub fn render_markdown(src: &str) -> String {
pub fn on_community_add_post(
    community: CommunityLocalID,
    post_local_id: PostLocalID,
    post_ap_id: &str,
    post_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    println!("on_community_add_post");


@@ 462,7 540,7 @@ pub fn on_community_add_post(
pub fn on_community_add_comment(
    community: CommunityLocalID,
    comment_local_id: CommentLocalID,
    comment_ap_id: &str,
    comment_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    crate::apub_util::spawn_announce_community_comment(


@@ 474,6 552,8 @@ pub fn on_community_add_comment(
}

pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteContext>) {
    use futures::future::TryFutureExt;

    println!("on_post_add_comment");
    spawn_task(async move {
        let db = ctx.db_pool.get().await?;


@@ 482,9 562,10 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
            db.query_opt(
                "SELECT community.id, community.local, community.ap_id, community.ap_inbox, post.local, post.ap_id, person.id, person.ap_id FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.id = $1 AND post.community = community.id",
                &[&comment.post.raw()],
            ),
            )
            .map_err(crate::Error::from),
            async {
                match comment.parent {
                let res: Result<Option<(BaseURL, Option<BaseURL>, bool, Option<UserLocalID>)>, crate::Error> = match comment.parent {
                    Some(parent) => {
                        let row = db.query_one(
                            "SELECT reply.local, reply.ap_id, person.id, person.ap_id FROM reply LEFT OUTER JOIN person ON (person.id = reply.author) WHERE reply.id=$1",


@@ 496,11 577,12 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
                        if row.get(0) {
                            Ok(Some((crate::apub_util::get_local_comment_apub_id(parent, &ctx.host_url_apub), Some(crate::apub_util::get_local_person_apub_id(UserLocalID(row.get(2)), &ctx.host_url_apub)), true, author_local_id)))
                        } else {
                            Ok(row.get::<_, Option<String>>(1).map(|x| (x, row.get(3), false, author_local_id)))
                            row.get::<_, Option<&str>>(1).map(|x: &str| -> Result<(BaseURL, Option<BaseURL>, bool, Option<UserLocalID>), crate::Error> { Ok((x.parse()?, row.get::<_, Option<&str>>(3).map(std::str::FromStr::from_str).transpose()?, false, author_local_id)) }).transpose()
                        }
                    },
                    None => Ok(None),
                }
                };
                res
            }
        ).await?;



@@ 514,14 596,17 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
                    &ctx.host_url_apub,
                ))
            } else {
                row.get(5)
                row.get::<_, Option<&str>>(5)
                    .map(std::str::FromStr::from_str)
                    .transpose()?
            };

            let comment_ap_id = match &comment.ap_id {
                crate::APIDOrLocal::APID(apid) => Cow::Borrowed(apid),
                crate::APIDOrLocal::Local => Cow::Owned(
                    crate::apub_util::get_local_comment_apub_id(comment.id, &ctx.host_url_apub),
                ),
                crate::APIDOrLocal::APID(apid) => apid.clone(),
                crate::APIDOrLocal::Local => {
                    crate::apub_util::get_local_comment_apub_id(comment.id, &ctx.host_url_apub)
                        .into()
                }
            };

            let (parent_ap_id, post_or_parent_author_local_id, post_or_parent_author_ap_id) =


@@ 532,18 617,19 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
                            (
                                None,
                                Some(author_id),
                                Some(Cow::<str>::Owned(
                                    crate::apub_util::get_local_person_apub_id(
                                        author_id,
                                        &ctx.host_url_apub,
                                    ),
                                )),
                                Some(Cow::Owned(crate::apub_util::get_local_person_apub_id(
                                    author_id,
                                    &ctx.host_url_apub,
                                ))),
                            )
                        } else {
                            (
                                None,
                                Some(author_id),
                                row.get::<_, Option<_>>(7).map(Cow::Borrowed),
                                row.get::<_, Option<_>>(7)
                                    .map(std::str::FromStr::from_str)
                                    .transpose()?
                                    .map(Cow::Owned),
                            )
                        }
                    }


@@ 552,7 638,7 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
                        Some((parent_ap_id, parent_author_ap_id, _, parent_author_local_id)) => (
                            Some(parent_ap_id),
                            *parent_author_local_id,
                            parent_author_ap_id.as_deref().map(Cow::Borrowed),
                            parent_author_ap_id.as_ref().map(Cow::Borrowed),
                        ),
                    },
                };


@@ 602,15 688,15 @@ pub fn on_post_add_comment(comment: CommentInfo<'static>, ctx: Arc<crate::RouteC
            if let Some(post_ap_id) = post_ap_id {
                if community_local {
                    let community = CommunityLocalID(row.get(0));
                    crate::on_community_add_comment(community, comment.id, &comment_ap_id, ctx);
                    crate::on_community_add_comment(community, comment.id, comment_ap_id, ctx);
                } else if comment.ap_id == APIDOrLocal::Local {
                    crate::apub_util::spawn_enqueue_send_comment_to_community(
                        comment,
                        row.get(2),
                        row.get(3),
                        post_ap_id,
                        parent_ap_id.cloned(),
                        post_or_parent_author_ap_id.as_deref(),
                        std::str::FromStr::from_str(row.get(2))?,
                        std::str::FromStr::from_str(row.get(3))?,
                        post_ap_id.into(),
                        parent_ap_id.map(|x| x.deref().clone()),
                        post_or_parent_author_ap_id.map(|x| x.into_owned().into()),
                        ctx,
                    );
                }


@@ 650,9 736,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
        _ => 3333,
    };

    let host_url_apub: url::Url = host_url_apub
        .parse()
        .expect("Failed to parse HOST_URL_ACTIVITYPUB");
    let host_url_apub: BaseURL = host_url_apub
        .try_into()
        .expect("HOST_URL_ACTIVITYPUB is not a valid base URL");

    let routes = Arc::new(routes::route_root());
    let base_context = Arc::new(BaseContext {
        local_hostname: get_url_host(&host_url_apub).expect("Failed to parse HOST_URL_ACTIVITYPUB"),
        local_hostname: get_url_host(&host_url_apub)
            .expect("Couldn't find host in HOST_URL_ACTIVITYPUB"),

        db_pool,
        host_url_api,

M src/routes/api/communities.rs => src/routes/api/communities.rs +3 -3
@@ 176,7 176,7 @@ async fn route_unstable_communities_get(
            host: if community_local {
                (&ctx.local_hostname).into()
            } else {
                match community_ap_id.and_then(crate::get_url_host) {
                match community_ap_id.and_then(crate::get_url_host_from_str) {
                    Some(host) => host.into(),
                    None => "[unknown]".into(),
                }


@@ 419,7 419,7 @@ async fn route_unstable_communities_posts_list(
            host: if community_local {
                (&ctx.local_hostname).into()
            } else {
                match community_ap_id.and_then(crate::get_url_host) {
                match community_ap_id.and_then(crate::get_url_host_from_str) {
                    Some(host) => host.into(),
                    None => "[unknown]".into(),
                }


@@ 459,7 459,7 @@ async fn route_unstable_communities_posts_list(
                    host: if author_local {
                        (&ctx.local_hostname).into()
                    } else {
                        match author_ap_id.and_then(crate::get_url_host) {
                        match author_ap_id.and_then(crate::get_url_host_from_str) {
                            Some(host) => host.into(),
                            None => "[unknown]".into(),
                        }

M src/routes/api/mod.rs => src/routes/api/mod.rs +18 -25
@@ 261,13 261,13 @@ async fn insert_token(
}

enum Lookup<'a> {
    URI(hyper::Uri),
    URL(url::Url),
    WebFinger { user: &'a str, host: &'a str },
}

fn parse_lookup(src: &str) -> Result<Lookup, crate::Error> {
    if src.starts_with("http") {
        return Ok(Lookup::URI(src.parse()?));
        return Ok(Lookup::URL(src.parse()?));
    }
    if let Some(at_idx) = src.rfind('@') {
        let mut user = &src[..at_idx];


@@ 299,7 299,7 @@ async fn route_unstable_actors_lookup(
    let lookup = parse_lookup(&query)?;

    let uri = match lookup {
        Lookup::URI(uri) => Some(uri),
        Lookup::URL(uri) => Some(uri),
        Lookup::WebFinger { user, host } => {
            let uri = format!(
                "https://{}/.well-known/webfinger?{}",


@@ 350,9 350,7 @@ async fn route_unstable_actors_lookup(
        }
    };

    let uri_str = uri.to_string();

    let actor = crate::apub_util::fetch_actor(&uri_str, &db, &ctx.http_client).await?;
    let actor = crate::apub_util::fetch_actor(&uri, &db, &ctx.http_client).await?;

    if let crate::apub_util::ActorLocalInfo::Community { id, .. } = actor {
        Ok(hyper::Response::builder()


@@ 637,7 635,7 @@ async fn route_unstable_posts_create(
                crate::on_community_add_post(
                    post.community,
                    post.id,
                    &crate::apub_util::get_local_post_apub_id(post.id, &ctx.host_url_apub),
                    crate::apub_util::get_local_post_apub_id(post.id, &ctx.host_url_apub).into(),
                    ctx,
                );
            } else {


@@ 1074,7 1072,7 @@ async fn route_unstable_posts_delete(
                        if let Some(community_inbox) = community_inbox {
                            crate::spawn_task(async move {
                                ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                                    inbox: community_inbox.into(),
                                    inbox: Cow::Owned(community_inbox.parse()?),
                                    sign_as: Some(crate::ActorLocalRef::Person(user)),
                                    object: serde_json::to_string(&delete_ap)?,
                                })


@@ 1116,13 1114,10 @@ async fn route_unstable_posts_like(
            ).await?;
            if let Some(row) = row {
                let post_local = row.get(0);
                let post_ap_id: &str = &if post_local {
                    Cow::Owned(crate::apub_util::get_local_post_apub_id(
                        post_id,
                        &ctx.host_url_apub,
                    ))
                let post_ap_id = if post_local {
                    crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub)
                } else {
                    Cow::Borrowed(row.get(1))
                    row.get::<_, &str>(1).parse()?
                };

                let mut inboxes = HashSet::new();


@@ 1153,7 1148,7 @@ async fn route_unstable_posts_like(

                for inbox in inboxes {
                    ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                        inbox: inbox.into(),
                        inbox: Cow::Owned(inbox.parse()?),
                        sign_as: Some(crate::ActorLocalRef::Person(user)),
                        object: (&body).into(),
                    })


@@ 1361,7 1356,7 @@ async fn route_unstable_posts_unlike(

                for inbox in inboxes {
                    ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                        inbox: inbox.into(),
                        inbox: Cow::Owned(inbox.parse()?),
                        sign_as: Some(crate::ActorLocalRef::Person(user)),
                        object: (&body).into(),
                    })


@@ 1633,7 1628,7 @@ async fn route_unstable_comments_delete(
                        if let Some(community_inbox) = community_inbox {
                            crate::spawn_task(async move {
                                ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                                    inbox: community_inbox.into(),
                                    inbox: Cow::Owned(community_inbox.parse()?),
                                    sign_as: Some(crate::ActorLocalRef::Person(user)),
                                    object: body,
                                })


@@ 1675,13 1670,11 @@ async fn route_unstable_comments_like(
            ).await?;
            if let Some(row) = row {
                let comment_local = row.get(0);
                let comment_ap_id: &str = &if comment_local {
                    Cow::Owned(crate::apub_util::get_local_comment_apub_id(
                        comment_id,
                        &ctx.host_url_apub,
                    ))
                let comment_ap_id = if comment_local {
                    crate::apub_util::get_local_comment_apub_id(comment_id, &ctx.host_url_apub)
                } else {
                    Cow::Borrowed(row.get(1))
                    let src: &str = row.get(1);
                    src.parse()?
                };

                let mut inboxes = HashSet::new();


@@ 1712,7 1705,7 @@ async fn route_unstable_comments_like(

                for inbox in inboxes {
                    ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                        inbox: inbox.into(),
                        inbox: Cow::Owned(inbox.parse()?),
                        sign_as: Some(crate::ActorLocalRef::Person(user)),
                        object: (&body).into(),
                    })


@@ 1920,7 1913,7 @@ async fn route_unstable_comments_unlike(

                for inbox in inboxes {
                    ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                        inbox: inbox.into(),
                        inbox: Cow::Owned(inbox.parse()?),
                        sign_as: Some(crate::ActorLocalRef::Person(user)),
                        object: (&body).into(),
                    })

M src/routes/apub/communities.rs => src/routes/apub/communities.rs +85 -107
@@ 1,6 1,7 @@
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use activitystreams::ext::Extensible;
use activitystreams::prelude::*;
use std::borrow::Cow;
use std::ops::Deref;
use std::sync::Arc;

pub fn route_communities() -> crate::RouteNode<()> {


@@ 113,32 114,35 @@ async fn handler_communities_get(
                crate::apub_util::get_local_community_apub_id(community_id, &ctx.host_url_apub);

            let mut info = activitystreams::actor::Group::new();
            info.as_mut()
                .set_many_context_xsd_any_uris(vec![
                    activitystreams::context(),
                    activitystreams::security(),
                ])?
                .set_id(community_ap_id.as_ref())?
                .set_name_xsd_string(name.as_ref())?
                .set_summary_xsd_string(description)?;

            let mut actor_props = activitystreams::actor::properties::ApActorProperties::default();

            actor_props.set_inbox(format!(
                "{}/communities/{}/inbox",
                ctx.host_url_apub, community_id
            ))?;
            actor_props.set_outbox(crate::apub_util::get_local_community_outbox_apub_id(
                community_id,
                &ctx.host_url_apub,
            ))?;
            actor_props.set_followers(format!(
                "{}/communities/{}/followers",
                ctx.host_url_apub, community_id
            ))?;
            actor_props.set_preferred_username(name)?;
            info.set_many_contexts(vec![
                activitystreams::context(),
                activitystreams::security(),
            ])
            .set_id(community_ap_id.deref().clone())
            .set_name(name.as_ref())
            .set_summary(description);

            let inbox = {
                let mut res = community_ap_id.clone();
                res.path_segments_mut().push("inbox");
                res
            };

            let info = info.extend(actor_props);
            let mut info = activitystreams::actor::ApActor::new(inbox.into(), info);

            info.set_outbox(
                crate::apub_util::get_local_community_outbox_apub_id(
                    community_id,
                    &ctx.host_url_apub,
                )
                .into(),
            )
            .set_followers({
                let mut res = community_ap_id.clone();
                res.path_segments_mut().push("followers");
                res.into()
            })
            .set_preferred_username(name);

            let key_id = format!(
                "{}/communities/{}#main-key",


@@ 149,13 153,13 @@ async fn handler_communities_get(
                let public_key_ext = crate::apub_util::PublicKeyExtension {
                    public_key: Some(crate::apub_util::PublicKey {
                        id: (&key_id).into(),
                        owner: (&community_ap_id).into(),
                        owner: community_ap_id.as_str().into(),
                        public_key_pem: public_key.into(),
                        signature_algorithm: Some(crate::apub_util::SIGALG_RSA_SHA256.into()),
                    }),
                };

                let info = info.extend(public_key_ext);
                let info = activitystreams_ext::Ext1::new(info, public_key_ext);

                serde_json::to_vec(&info)
            } else {


@@ 203,12 207,12 @@ async fn handler_communities_comments_announce_get(

            let comment_local_id = CommentLocalID(row.get(0));
            let comment_ap_id = if row.get(1) {
                Cow::Owned(crate::apub_util::get_local_comment_apub_id(comment_local_id, &ctx.host_url_apub))
                crate::apub_util::get_local_comment_apub_id(comment_local_id, &ctx.host_url_apub)
            } else {
                Cow::Borrowed(row.get(2))
                std::str::FromStr::from_str(row.get(2))?
            };

            let body = crate::apub_util::local_community_comment_announce_ap(community_id, comment_local_id, &comment_ap_id, &ctx.host_url_apub)?;
            let body = crate::apub_util::local_community_comment_announce_ap(community_id, comment_local_id, comment_ap_id.into(), &ctx.host_url_apub)?;
            let body = serde_json::to_vec(&body)?;

            Ok(hyper::Response::builder()


@@ 277,35 281,33 @@ async fn handler_communities_followers_get(
            let community_ap_id = if community_local {
                crate::apub_util::get_local_community_apub_id(community_id, &ctx.host_url_apub)
            } else {
                let community_ap_id: Option<String> = row.get(2);
                community_ap_id.ok_or_else(|| {
                let community_ap_id: Option<&str> = row.get(2);
                std::str::FromStr::from_str(community_ap_id.ok_or_else(|| {
                    crate::Error::InternalStr(format!(
                        "Missing ap_id for community {}",
                        community_id
                    ))
                })?
                })?)?
            };

            let mut follow = activitystreams::activity::Follow::new();

            follow
                .object_props
                .set_context_xsd_any_uri(activitystreams::context())?;

            follow.object_props.set_id(format!(
                "{}/communities/{}/followers/{}",
                ctx.host_url_apub, community_id, user_id
            ))?;

            let person_ap_id =
                crate::apub_util::get_local_person_apub_id(user_id, &ctx.host_url_apub);

            follow.follow_props.set_actor_xsd_any_uri(person_ap_id)?;
            let mut follow =
                activitystreams::activity::Follow::new(person_ap_id, community_ap_id.clone());

            follow
                .follow_props
                .set_object_xsd_any_uri(community_ap_id.as_ref())?;
            follow.object_props.set_to_xsd_any_uri(community_ap_id)?;
                .set_context(activitystreams::context())
                .set_id({
                    let mut res = crate::apub_util::get_local_community_apub_id(
                        community_id,
                        &ctx.host_url_apub,
                    );
                    res.path_segments_mut()
                        .extend(&["followers", &user_id.to_string()]);
                    res.into()
                })
                .set_to(community_ap_id);

            let body = serde_json::to_vec(&follow)?.into();



@@ 345,11 347,11 @@ async fn handler_communities_followers_accept_get(

            let follower_local = row.get(3);
            let follow_ap_id = if follower_local {
                Cow::Owned(crate::apub_util::get_local_follow_apub_id(
                crate::apub_util::get_local_follow_apub_id(
                    community_id,
                    UserLocalID(row.get(2)),
                    &ctx.host_url_apub,
                ))
                )
            } else {
                let follow_ap_id: Option<&str> = row.get(1);
                follow_ap_id


@@ 359,13 361,13 @@ async fn handler_communities_followers_accept_get(
                            community_id, user_id
                        ))
                    })?
                    .into()
                    .parse()?
            };

            let body = crate::apub_util::community_follow_accept_to_ap(
                &crate::apub_util::get_local_community_apub_id(community_id, &ctx.host_url_apub),
                crate::apub_util::get_local_community_apub_id(community_id, &ctx.host_url_apub),
                user_id,
                &follow_ap_id,
                follow_ap_id.into(),
            )?;
            let body = serde_json::to_vec(&body)?.into();



@@ 381,10 383,12 @@ async fn handler_communities_inbox_post(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    use crate::apub_util::{KnownObject, Verified};

    let (community_id,) = params;
    let db = ctx.db_pool.get().await?;

    let activity = crate::apub_util::verify_incoming_activity(
    let object = crate::apub_util::verify_incoming_object(
        req,
        &db,
        &ctx.http_client,


@@ 392,38 396,25 @@ async fn handler_communities_inbox_post(
    )
    .await?;

    match activity.kind() {
        Some("Create") => {
            super::inbox_common_create(
                activity
                    .into_concrete_activity::<activitystreams::activity::Create>()
                    .unwrap(),
                ctx,
            )
            .await?;
    match object.into_inner() {
        KnownObject::Create(create) => {
            super::inbox_common_create(Verified(create), ctx).await?;
        }
        Some("Follow") => {
            let follow = activity
                .into_concrete_activity::<activitystreams::activity::Follow>()
                .unwrap();

            let follower_ap_id = follow.follow_props.get_actor_xsd_any_uri();
            let target_community = follow.follow_props.get_object_xsd_any_uri();
        KnownObject::Follow(follow) => {
            let follow = Verified(follow);
            let follower_ap_id = follow.actor_unchecked().as_single_id();
            let target_community = follow.object().as_single_id();

            if let Some(follower_ap_id) = follower_ap_id {
                let activity_ap_id = follow
                    .object_props
                    .get_id()
                    .id_unchecked()
                    .ok_or(crate::Error::InternalStrStatic("Missing activitity ID"))?;

                crate::apub_util::require_containment(
                    activity_ap_id.as_url(),
                    follower_ap_id.as_url(),
                )?;
                crate::apub_util::require_containment(activity_ap_id, follower_ap_id)?;
                let follow = crate::apub_util::Contained(Cow::Borrowed(&follow));

                let follower_local_id = crate::apub_util::get_or_fetch_user_local_id(
                    follower_ap_id.as_str(),
                    follower_ap_id,
                    &db,
                    &ctx.host_url_apub,
                    &ctx.http_client,


@@ 431,11 422,12 @@ async fn handler_communities_inbox_post(
                .await?;

                if let Some(target_community) = target_community {
                    if target_community.as_str()
                    if target_community
                        == crate::apub_util::get_local_community_apub_id(
                            community_id,
                            &ctx.host_url_apub,
                        )
                        .deref()
                    {
                        let row = db
                            .query_opt("SELECT local FROM community WHERE id=$1", &[&community_id])


@@ 461,25 453,14 @@ async fn handler_communities_inbox_post(
                }
            }
        }
        Some("Delete") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Delete>()
                .unwrap();

            crate::apub_util::handle_delete(activity, ctx).await?;
        KnownObject::Delete(activity) => {
            crate::apub_util::handle_delete(Verified(activity), ctx).await?;
        }
        Some("Like") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Like>()
                .unwrap();

            crate::apub_util::handle_like(activity, ctx).await?;
        KnownObject::Like(activity) => {
            crate::apub_util::handle_like(Verified(activity), ctx).await?;
        }
        Some("Undo") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Undo>()
                .unwrap();
            crate::apub_util::handle_undo(activity, ctx).await?;
        KnownObject::Undo(activity) => {
            crate::apub_util::handle_undo(Verified(activity), ctx).await?;
        }
        _ => {}
    }


@@ 501,7 482,7 @@ async fn handler_communities_outbox_get(

    let collection = serde_json::json!({
        "@context": activitystreams::context(),
        "type": activitystreams::collection::kind::OrderedCollectionType,
        "type": activitystreams::collection::kind::OrderedCollectionType::OrderedCollection,
        "id": crate::apub_util::get_local_community_outbox_apub_id(community_id, &ctx.host_url_apub),
        "first": &page_ap_id,
        "current": &page_ap_id


@@ 551,18 532,15 @@ async fn handler_communities_outbox_page_get(
        .map(|row| {
            let post_id = PostLocalID(row.get(0));
            let post_ap_id = if row.get(1) {
                Cow::Owned(crate::apub_util::get_local_post_apub_id(
                    post_id,
                    &ctx.host_url_apub,
                ))
                crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub)
            } else {
                Cow::Borrowed(row.get(2))
                std::str::FromStr::from_str(row.get(2))?
            };

            crate::apub_util::local_community_post_announce_ap(
                community_id,
                post_id,
                &post_ap_id,
                post_ap_id.into(),
                &ctx.host_url_apub,
            )
        })


@@ 581,7 559,7 @@ async fn handler_communities_outbox_page_get(

    let info = serde_json::json!({
        "@context": activitystreams::context(),
        "type": activitystreams::collection::kind::OrderedCollectionPageType,
        "type": activitystreams::collection::kind::OrderedCollectionPageType::OrderedCollectionPage,
        "partOf": crate::apub_util::get_local_community_outbox_apub_id(community_id, &ctx.host_url_apub),
        "orderedItems": items,
        "next": next,


@@ 626,15 604,15 @@ async fn handler_communities_posts_announce_get(
                Some(true) => {
                    let post_local_id = PostLocalID(row.get(0));
                    let post_ap_id = if row.get(1) {
                        Cow::Owned(crate::apub_util::get_local_post_apub_id(post_local_id, &ctx.host_url_apub))
                        crate::apub_util::get_local_post_apub_id(post_local_id, &ctx.host_url_apub)
                    } else {
                        Cow::Borrowed(row.get(2))
                        std::str::FromStr::from_str(row.get(2))?
                    };

                    let body = crate::apub_util::local_community_post_announce_ap(
                        community_id,
                        post_local_id,
                        &post_ap_id,
                        post_ap_id.into(),
                        &ctx.host_url_apub,
                    )?;
                    let body = serde_json::to_vec(&body)?;

M src/routes/apub/mod.rs => src/routes/apub/mod.rs +137 -239
@@ 1,7 1,7 @@
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use activitystreams::ext::Extensible;
use serde_derive::Deserialize;
use activitystreams::prelude::*;
use std::borrow::Cow;
use std::ops::Deref;
use std::sync::Arc;

mod communities;


@@ 104,31 104,6 @@ pub fn route_apub() -> crate::RouteNode<()> {
        )
}

fn get_object_id(
    aao_props: &activitystreams::activity::properties::ActorAndObjectProperties,
) -> Result<Option<Cow<'_, activitystreams::primitives::XsdAnyUri>>, crate::Error> {
    match aao_props.get_object_xsd_any_uri() {
        Some(uri) => Ok(Some(Cow::Borrowed(uri))),
        None => match aao_props.get_object_base_box() {
            Some(base_box) => {
                #[derive(Deserialize)]
                struct WithMaybeID {
                    id: Option<activitystreams::primitives::XsdAnyUri>,
                }

                impl activitystreams::Base for WithMaybeID {}

                let value: WithMaybeID = base_box.clone().into_concrete()?;
                match value.id {
                    Some(id) => Ok(Some(Cow::Owned(id))),
                    None => Ok(None),
                }
            }
            None => Ok(None),
        },
    }
}

pub fn route_inbox() -> crate::RouteNode<()> {
    crate::RouteNode::new().with_handler_async("POST", handler_inbox_post)
}


@@ 179,28 154,35 @@ async fn handler_users_get(
                crate::apub_util::get_local_person_apub_id(user_id, &ctx.host_url_apub);

            let mut info = activitystreams::actor::Person::new();
            info.object_props.set_many_context_xsd_any_uris(vec![
            info.set_many_contexts(vec![
                activitystreams::context(),
                activitystreams::security(),
            ])?;
            info.as_mut()
                .set_id(user_ap_id.as_ref())?
                .set_name_xsd_string(username.as_ref())?
                .set_summary_xsd_string(description)?;

            let mut endpoints = activitystreams::endpoint::EndpointProperties::default();
            endpoints.set_shared_inbox(format!("{}/inbox", ctx.host_url_apub))?;

            let mut actor_props = activitystreams::actor::properties::ApActorProperties::default();
            actor_props.set_inbox(format!("{}/users/{}/inbox", ctx.host_url_apub, user_id))?;
            actor_props.set_outbox(crate::apub_util::get_local_person_outbox_apub_id(
                user_id,
                &ctx.host_url_apub,
            ))?;
            actor_props.set_endpoints(endpoints)?;
            actor_props.set_preferred_username(username)?;
            ]);
            info.set_id(user_ap_id.deref().clone())
                .set_name(username.as_ref())
                .set_summary(description);

            let endpoints = activitystreams::actor::Endpoints {
                shared_inbox: Some(
                    crate::apub_util::get_local_shared_inbox(&ctx.host_url_apub).into(),
                ),
                ..Default::default()
            };

            let info = info.extend(actor_props);
            let mut info = activitystreams::actor::ApActor::new(
                {
                    let mut res = user_ap_id.clone();
                    res.path_segments_mut().push("inbox");
                    res.into()
                },
                info,
            );
            info.set_outbox(
                crate::apub_util::get_local_person_outbox_apub_id(user_id, &ctx.host_url_apub)
                    .into(),
            )
            .set_endpoints(endpoints)
            .set_preferred_username(username);

            let key_id = format!("{}/users/{}#main-key", ctx.host_url_apub, user_id);



@@ 208,13 190,13 @@ async fn handler_users_get(
                let public_key_ext = crate::apub_util::PublicKeyExtension {
                    public_key: Some(crate::apub_util::PublicKey {
                        id: (&key_id).into(),
                        owner: (&user_ap_id).into(),
                        owner: user_ap_id.as_str().into(),
                        public_key_pem: public_key.into(),
                        signature_algorithm: Some(crate::apub_util::SIGALG_RSA_SHA256.into()),
                    }),
                };

                let info = info.extend(public_key_ext);
                let info = activitystreams_ext::Ext1::new(info, public_key_ext);

                serde_json::to_vec(&info)
            } else {


@@ 236,9 218,11 @@ async fn inbox_common(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    use crate::apub_util::{KnownObject, Verified};

    let db = ctx.db_pool.get().await?;

    let activity = crate::apub_util::verify_incoming_activity(
    let object = crate::apub_util::verify_incoming_object(
        req,
        &db,
        &ctx.http_client,


@@ 246,23 230,18 @@ async fn inbox_common(
    )
    .await?;

    match activity.kind() {
        Some("Accept") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Accept>()
                .unwrap();

    match object.into_inner() {
        KnownObject::Accept(activity) => {
            let activity_id = activity
                .object_props
                .get_id()
                .id_unchecked()
                .ok_or(crate::Error::InternalStrStatic("Missing activity ID"))?;

            let actor_ap_id = activity
                .accept_props
                .get_actor_xsd_any_uri()
                .actor_unchecked()
                .as_single_id()
                .ok_or(crate::Error::InternalStrStatic("Missing actor for Accept"))?;

            crate::apub_util::require_containment(activity_id.as_url(), actor_ap_id.as_url())?;
            crate::apub_util::require_containment(activity_id, actor_ap_id)?;

            let actor_ap_id = actor_ap_id.as_str();



@@ 273,13 252,13 @@ async fn inbox_common(
            };

            if let Some(community_local_id) = community_local_id {
                let object_id = get_object_id(&activity.accept_props)?
                let object_id = activity
                    .object()
                    .as_single_id()
                    .ok_or(crate::Error::InternalStrStatic("Missing object for Accept"))?;

                let object_id = object_id.as_str();

                if object_id.starts_with(&ctx.host_url_apub) {
                    let remaining = &object_id[ctx.host_url_apub.len()..];
                if object_id.as_str().starts_with(&ctx.host_url_apub.as_str()) {
                    let remaining = &object_id.as_str()[ctx.host_url_apub.as_str().len()..];
                    if remaining.starts_with("/communities/") {
                        let remaining = &remaining[13..];
                        let next_expected = format!("{}/followers/", community_local_id);


@@ 296,16 275,12 @@ async fn inbox_common(
                }
            }
        }
        Some("Announce") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Announce>()
                .unwrap();
        KnownObject::Announce(activity) => {
            let activity_id = activity
                .object_props
                .get_id()
                .id_unchecked()
                .ok_or(crate::Error::InternalStrStatic("Missing activity ID"))?;

            let community_ap_id = activity.announce_props.get_actor_xsd_any_uri().ok_or(
            let community_ap_id = activity.actor_unchecked().as_single_id().ok_or(
                crate::Error::InternalStrStatic("Missing actor for Announce"),
            )?;



@@ 318,47 293,15 @@ async fn inbox_common(
                .map(|row| (CommunityLocalID(row.get(0)), row.get(1)));

            if let Some((community_local_id, community_is_local)) = community_local_info {
                crate::apub_util::require_containment(
                    activity_id.as_url(),
                    community_ap_id.as_url(),
                )?;

                let object_id = {
                    if let activitystreams::activity::properties::ActorAndObjectOptTargetPropertiesObjectEnum::Term(
                        req_obj,
                    ) = activity.into_inner().announce_props.object
                    {
                        match req_obj {
                            activitystreams::activity::properties::ActorAndObjectOptTargetPropertiesObjectTermEnum::XsdAnyUri(id) => Some(id),
                            activitystreams::activity::properties::ActorAndObjectOptTargetPropertiesObjectTermEnum::BaseBox(req_obj) => {
                                match req_obj.kind() {
                                    Some("Page") => {
                                        let req_obj = req_obj.into_concrete::<activitystreams::object::Page>().unwrap();

                                        Some(req_obj.object_props.id.ok_or_else(|| crate::Error::UserError(crate::simple_response(hyper::StatusCode::BAD_REQUEST, "Missing id in object")))?)
                                    },
                                    Some("Note") => {
                                        let req_obj = req_obj.into_concrete::<activitystreams::object::Note>().unwrap();

                                        Some(req_obj.object_props.id.ok_or_else(|| crate::Error::UserError(crate::simple_response(hyper::StatusCode::BAD_REQUEST, "Missing id in object")))?)
                                    },
                                    _ => None,
                                }
                            }
                        }
                    } else {
                        None
                    }
                };
                crate::apub_util::require_containment(activity_id, community_ap_id)?;

                let object_id = activity.object().as_single_id();

                if let Some(object_id) = object_id {
                    if !object_id.as_str().starts_with(&ctx.host_url_apub) {
                    if !object_id.as_str().starts_with(&ctx.host_url_apub.as_str()) {
                        // don't need announces for local objects
                        let body =
                            crate::apub_util::fetch_ap_object(object_id.as_str(), &ctx.http_client)
                                .await?;
                        let obj: activitystreams::object::ObjectBox = serde_json::from_value(body)?;
                        let obj = crate::apub_util::Verified(obj);
                        let obj =
                            crate::apub_util::fetch_ap_object(object_id, &ctx.http_client).await?;
                        crate::apub_util::handle_recieved_object_for_community(
                            community_local_id,
                            community_is_local,


@@ 370,60 313,43 @@ async fn inbox_common(
                }
            }
        }
        Some("Create") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Create>()
                .unwrap();
            inbox_common_create(activity, ctx).await?;
        KnownObject::Create(activity) => inbox_common_create(Verified(activity), ctx).await?,
        KnownObject::Delete(activity) => {
            crate::apub_util::handle_delete(Verified(activity), ctx).await?
        }
        Some("Delete") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Delete>()
                .unwrap();

            crate::apub_util::handle_delete(activity, ctx).await?;
        }
        Some("Like") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Like>()
                .unwrap();
            crate::apub_util::handle_like(activity, ctx).await?;
        KnownObject::Like(activity) => {
            crate::apub_util::handle_like(Verified(activity), ctx).await?
        }
        Some("Undo") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Undo>()
                .unwrap();
            crate::apub_util::handle_undo(activity, ctx).await?;
        KnownObject::Undo(activity) => {
            crate::apub_util::handle_undo(Verified(activity), ctx).await?
        }
        Some("Update") => {
            let activity = activity
                .into_concrete_activity::<activitystreams::activity::Update>()
                .unwrap();

        KnownObject::Update(activity) => {
            let activity_id = activity
                .object_props
                .get_id()
                .id_unchecked()
                .ok_or(crate::Error::InternalStrStatic("Missing activity ID"))?;

            let object_id = activity
                .update_props
                .get_object_xsd_any_uri()
                .ok_or(crate::Error::InternalStrStatic("Missing object for Update"))?;
            let object_id =
                activity
                    .object()
                    .as_single_id()
                    .ok_or(crate::Error::InternalStrStatic(
                        "Missing object ID for Update",
                    ))?;

            crate::apub_util::require_containment(activity_id.as_url(), object_id.as_url())?;
            crate::apub_util::require_containment(activity_id, object_id)?;

            let object_id = object_id.as_str().to_owned();
            let object_id = object_id.clone();

            crate::spawn_task(async move {
                let row = db
                    .query_opt(
                        "SELECT 1 FROM community WHERE ap_id=$1 LIMIT 1",
                        &[&object_id],
                        &[&object_id.as_str()],
                    )
                    .await?;
                if row.is_some() {
                    ctx.enqueue_task(&crate::tasks::FetchActor {
                        actor_ap_id: object_id.into(),
                        actor_ap_id: Cow::Owned(object_id),
                    })
                    .await?;
                }


@@ 441,36 367,13 @@ pub async fn inbox_common_create(
    activity: crate::apub_util::Verified<activitystreams::activity::Create>,
    ctx: Arc<crate::RouteContext>,
) -> Result<(), crate::Error> {
    let req_obj = activity.into_inner().create_props.object;
    if let activitystreams::activity::properties::ActorAndObjectPropertiesObjectEnum::Term(
        req_obj,
    ) = req_obj
    {
        let object_id = match req_obj {
            activitystreams::activity::properties::ActorAndObjectPropertiesObjectTermEnum::XsdAnyUri(id) => Some(id),
            activitystreams::activity::properties::ActorAndObjectPropertiesObjectTermEnum::BaseBox(req_obj) => {
                match req_obj.kind() {
                    Some("Page") => {
                        let req_obj = req_obj.into_concrete::<activitystreams::object::Page>().unwrap();

                        Some(req_obj.object_props.id.ok_or_else(|| crate::Error::UserError(crate::simple_response(hyper::StatusCode::BAD_REQUEST, "Missing id in object")))?)
                    },
                    Some("Note") => {
                        let req_obj = req_obj.into_concrete::<activitystreams::object::Note>().unwrap();
    for req_obj in activity.object().iter() {
        let object_id = req_obj.id();

                        Some(req_obj.object_props.id.ok_or_else(|| crate::Error::UserError(crate::simple_response(hyper::StatusCode::BAD_REQUEST, "Missing id in object")))?)
                    },
                    _ => None,
                }
            }
        };
        if let Some(object_id) = object_id {
            let body =
                crate::apub_util::fetch_ap_object(object_id.as_str(), &ctx.http_client).await?;
            let obj: activitystreams::object::ObjectBox = serde_json::from_value(body)?;
            let obj = crate::apub_util::Verified(obj);
            let obj = crate::apub_util::fetch_ap_object(object_id, &ctx.http_client).await?;

            crate::apub_util::handle_recieved_object_for_local_community(obj, ctx).await?;
            crate::apub_util::handle_recieved_object_for_local_community(obj, ctx.clone()).await?;
        }
    }



@@ 499,7 402,7 @@ async fn handler_users_outbox_get(

    let collection = serde_json::json!({
        "@context": activitystreams::context(),
        "type": activitystreams::collection::kind::OrderedCollectionType,
        "type": activitystreams::collection::kind::OrderedCollectionType::OrderedCollection,
        "id": crate::apub_util::get_local_person_outbox_apub_id(user, &ctx.host_url_apub),
        "first": &page_ap_id,
        "current": &page_ap_id


@@ 550,12 453,9 @@ async fn handler_users_outbox_page_get(
                let community_id = CommunityLocalID(row.get(8));
                let community_local = row.get(9);
                let community_ap_id = if community_local {
                    Cow::Owned(crate::apub_util::get_local_community_apub_id(
                        community_id,
                        &ctx.host_url_apub,
                    ))
                    crate::apub_util::get_local_community_apub_id(community_id, &ctx.host_url_apub)
                } else {
                    Cow::Borrowed(row.get(10))
                    row.get::<_, &str>(10).parse()?
                };

                let post_info = crate::PostInfo {


@@ 572,7 472,7 @@ async fn handler_users_outbox_page_get(

                let res = crate::apub_util::local_post_to_create_ap(
                    &post_info,
                    &community_ap_id,
                    community_ap_id.into(),
                    &ctx.host_url_apub,
                );
                last_created = Some(created);


@@ 598,38 498,38 @@ async fn handler_users_outbox_page_get(
                let res = crate::apub_util::local_comment_to_create_ap(
                    &comment_info,
                    &(if row.get(9) {
                        Cow::Owned(crate::apub_util::get_local_post_apub_id(
                            post_id,
                            &ctx.host_url_apub,
                        ))
                        crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub).into()
                    } else {
                        Cow::Borrowed(row.get(10))
                        std::str::FromStr::from_str(row.get(10))?
                    }),
                    match row.get(12) {
                        Some(true) => Some(Cow::Owned(
                            crate::apub_util::get_local_comment_apub_id(id, &ctx.host_url_apub),
                        )),
                        Some(false) => Some(Cow::Borrowed(row.get(7))),
                        Some(true) => Some(
                            crate::apub_util::get_local_comment_apub_id(id, &ctx.host_url_apub)
                                .into(),
                        ),
                        Some(false) => Some(std::str::FromStr::from_str(row.get(7))?),
                        None => None,
                    }
                    .as_deref(),
                    },
                    match row.get(14) {
                        Some(true) => Some(Cow::Owned(crate::apub_util::get_local_person_apub_id(
                            UserLocalID(row.get(13)),
                            &ctx.host_url_apub,
                        ))),
                        Some(false) => Some(Cow::Borrowed(row.get(5))),
                        Some(true) => Some(
                            crate::apub_util::get_local_person_apub_id(
                                UserLocalID(row.get(13)),
                                &ctx.host_url_apub,
                            )
                            .into(),
                        ),
                        Some(false) => Some(std::str::FromStr::from_str(row.get(5))?),
                        None => None,
                    }
                    .as_deref(),
                    &(if row.get(16) {
                        Cow::Owned(crate::apub_util::get_local_community_apub_id(
                    },
                    if row.get(16) {
                        crate::apub_util::get_local_community_apub_id(
                            CommunityLocalID(row.get(15)),
                            &ctx.host_url_apub,
                        ))
                        )
                        .into()
                    } else {
                        Cow::Borrowed(row.get(17))
                    }),
                        std::str::FromStr::from_str(row.get(17))?
                    },
                    &ctx.host_url_apub,
                )
                .and_then(|x| Ok(x.try_into()?));


@@ 653,7 553,7 @@ async fn handler_users_outbox_page_get(

    let info = serde_json::json!({
        "@context": activitystreams::context(),
        "type": activitystreams::collection::kind::OrderedCollectionPageType,
        "type": activitystreams::collection::kind::OrderedCollectionPageType::OrderedCollectionPage,
        "partOf": crate::apub_util::get_local_person_outbox_apub_id(user, &ctx.host_url_apub),
        "orderedItems": items,
        "next": next,


@@ 697,9 597,10 @@ async fn handler_comments_get(

            if row.get(19) {
                let mut body = activitystreams::object::Tombstone::new();
                body.tombstone_props.set_former_type_xsd_string("Note")?;
                body.object_props.set_context_xsd_any_uri(activitystreams::context())?;
                body.object_props.set_id(crate::apub_util::get_local_comment_apub_id(comment_id, &ctx.host_url_apub))?;
                body
                    .set_former_type("Note".to_owned())
                    .set_context(activitystreams::context())
                    .set_id(crate::apub_util::get_local_comment_apub_id(comment_id, &ctx.host_url_apub).into());

                let body = serde_json::to_vec(&body)?.into();



@@ 720,13 621,13 @@ async fn handler_comments_get(
            let community_ap_id = if row.get(9) {
                crate::apub_util::get_local_community_apub_id(community_local_id, &ctx.host_url_apub)
            } else {
                row.get(10)
                std::str::FromStr::from_str(row.get(10))?
            };

            let post_ap_id = if row.get(6) {
                crate::apub_util::get_local_post_apub_id(post_local_id, &ctx.host_url_apub)
            } else {
                row.get(7)
                std::str::FromStr::from_str(row.get(7))?
            };

            let parent_local_id = row.get::<_, Option<_>>(5).map(CommentLocalID);


@@ 751,7 652,7 @@ async fn handler_comments_get(
            let parent_ap_id = match row.get(11) {
                None => None,
                Some(true) => Some(crate::apub_util::get_local_comment_apub_id(parent_local_id.unwrap(), &ctx.host_url_apub)),
                Some(false) => row.get(12),
                Some(false) => row.get::<_, Option<&str>>(12).map(|x| x.parse()).transpose()?,
            };

            let post_or_parent_author_ap_id = match parent_local_id {


@@ 762,7 663,7 @@ async fn handler_comments_get(
                            if post_author_local {
                                Some(crate::apub_util::get_local_person_apub_id(UserLocalID(row.get(13)), &ctx.host_url_apub))
                            } else {
                                row.get(15)
                                Some(std::str::FromStr::from_str(row.get(15))?)
                            }
                        },
                        None => None,


@@ 774,7 675,7 @@ async fn handler_comments_get(
                            if parent_author_local {
                                Some(crate::apub_util::get_local_person_apub_id(UserLocalID(row.get(16)), &ctx.host_url_apub))
                            } else {
                                row.get(18)
                                Some(std::str::FromStr::from_str(row.get(18))?)
                            }
                        },
                        None => None,


@@ 782,7 683,7 @@ async fn handler_comments_get(
                },
            };

            let body = crate::apub_util::local_comment_to_ap(&info, &post_ap_id, parent_ap_id.as_deref(), post_or_parent_author_ap_id.as_deref(), &community_ap_id, &ctx.host_url_apub)?;
            let body = crate::apub_util::local_comment_to_ap(&info, &post_ap_id, parent_ap_id.map(From::from), post_or_parent_author_ap_id.map(From::from), community_ap_id.into(), &ctx.host_url_apub)?;

            let body = serde_json::to_vec(&body)?.into();



@@ 840,13 741,13 @@ async fn handler_comments_create_get(
            let community_ap_id = if row.get(9) {
                crate::apub_util::get_local_community_apub_id(community_local_id, &ctx.host_url_apub)
            } else {
                row.get(10)
                std::str::FromStr::from_str(row.get(10))?
            };

            let post_ap_id = if row.get(6) {
                crate::apub_util::get_local_post_apub_id(post_local_id, &ctx.host_url_apub)
            } else {
                row.get(7)
                std::str::FromStr::from_str(row.get(7))?
            };

            let parent_local_id = row.get::<_, Option<_>>(5).map(CommentLocalID);


@@ 870,7 771,7 @@ async fn handler_comments_create_get(
            let parent_ap_id = match row.get(11) {
                None => None,
                Some(true) => Some(crate::apub_util::get_local_comment_apub_id(parent_local_id.unwrap(), &ctx.host_url_apub)),
                Some(false) => row.get(12),
                Some(false) => row.get::<_, Option<&str>>(12).map(std::str::FromStr::from_str).transpose()?,
            };

            let post_or_parent_author_ap_id = match parent_local_id {


@@ 881,7 782,7 @@ async fn handler_comments_create_get(
                            if post_author_local {
                                Some(crate::apub_util::get_local_person_apub_id(UserLocalID(row.get(13)), &ctx.host_url_apub))
                            } else {
                                row.get(15)
                                Some(std::str::FromStr::from_str(row.get(15))?)
                            }
                        },
                        None => None,


@@ 893,7 794,7 @@ async fn handler_comments_create_get(
                            if parent_author_local {
                                Some(crate::apub_util::get_local_person_apub_id(UserLocalID(row.get(16)), &ctx.host_url_apub))
                            } else {
                                row.get(18)
                                Some(std::str::FromStr::from_str(row.get(18))?)
                            }
                        },
                        None => None,


@@ 901,7 802,7 @@ async fn handler_comments_create_get(
                },
            };

            let body = crate::apub_util::local_comment_to_create_ap(&info, &post_ap_id, parent_ap_id.as_deref(), post_or_parent_author_ap_id.as_deref(), &community_ap_id, &ctx.host_url_apub)?;
            let body = crate::apub_util::local_comment_to_create_ap(&info, &post_ap_id, parent_ap_id.map(From::from), post_or_parent_author_ap_id.map(From::from), community_ap_id.into(), &ctx.host_url_apub)?;

            let body = serde_json::to_vec(&body)?.into();



@@ 997,17 898,14 @@ async fn handler_comments_likes_get(
                .await?;
            let comment_local = row.get(0);
            let comment_ap_id = if comment_local {
                Cow::Owned(crate::apub_util::get_local_comment_apub_id(
                    comment_id,
                    &ctx.host_url_apub,
                ))
                crate::apub_util::get_local_comment_apub_id(comment_id, &ctx.host_url_apub)
            } else {
                Cow::Borrowed(row.get(1))
                std::str::FromStr::from_str(row.get(1))?
            };

            let like = crate::apub_util::local_comment_like_to_ap(
                comment_id,
                &comment_ap_id,
                comment_ap_id,
                user_id,
                &ctx.host_url_apub,
            )?;


@@ 1151,9 1049,12 @@ async fn handler_posts_get(
                let had_href: Option<bool> = row.get(7);

                let mut body = activitystreams::object::Tombstone::new();
                body.tombstone_props.set_former_type_xsd_string(if had_href == Some(true) { "Page" } else { "Note" })?;
                body.object_props.set_context_xsd_any_uri(activitystreams::context())?;
                body.object_props.set_id(crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub))?;
                body
                    .set_former_type(
                        (if had_href == Some(true) { "Page" } else { "Note" }).to_owned()
                    )
                    .set_context(activitystreams::context())
                    .set_id(crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub).into());

                let body = serde_json::to_vec(&body)?.into();



@@ 1170,7 1071,7 @@ async fn handler_posts_get(
            let community_local_id = CommunityLocalID(row.get(4));

            let community_ap_id = match row.get(11) {
                Some(ap_id) => ap_id,
                Option::<&str>::Some(ap_id) => ap_id.parse()?,
                None => {
                    // assume local (might be a problem?)
                    crate::apub_util::get_local_community_apub_id(community_local_id, &ctx.host_url_apub)


@@ 1189,7 1090,7 @@ async fn handler_posts_get(
                title: row.get(2),
            };

            let body = crate::apub_util::post_to_ap(&post_info, &community_ap_id, &ctx.host_url_apub)?;
            let body = crate::apub_util::post_to_ap(&post_info, community_ap_id.into(), &ctx.host_url_apub)?;

            let body = serde_json::to_vec(&body)?.into();



@@ 1244,7 1145,7 @@ async fn handler_posts_create_get(
            let community_local_id = CommunityLocalID(row.get(4));

            let community_ap_id = match row.get(10) {
                Some(ap_id) => ap_id,
                Option::<&str>::Some(ap_id) => ap_id.parse()?,
                None => {
                    // assume local (might be a problem?)
                    crate::apub_util::get_local_community_apub_id(community_local_id, &ctx.host_url_apub)


@@ 1263,7 1164,7 @@ async fn handler_posts_create_get(
                title: row.get(3),
            };

            let body = crate::apub_util::local_post_to_create_ap(&post_info, &community_ap_id, &ctx.host_url_apub)?;
            let body = crate::apub_util::local_post_to_create_ap(&post_info, community_ap_id.into(), &ctx.host_url_apub)?;

            let body = serde_json::to_vec(&body)?.into();



@@ 1360,17 1261,14 @@ async fn handler_posts_likes_get(
                .await?;
            let post_local = row.get(0);
            let post_ap_id = if post_local {
                Cow::Owned(crate::apub_util::get_local_post_apub_id(
                    post_id,
                    &ctx.host_url_apub,
                ))
                crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub)
            } else {
                Cow::Borrowed(row.get(1))
                std::str::FromStr::from_str(row.get(1))?
            };

            let like = crate::apub_util::local_post_like_to_ap(
                post_id,
                &post_ap_id,
                post_ap_id,
                user_id,
                &ctx.host_url_apub,
            )?;

M src/routes/well_known.rs => src/routes/well_known.rs +5 -4
@@ 72,8 72,8 @@ async fn handler_webfinger_get(
        Name(&'a str),
    }

    let found_ref = if query.resource.starts_with(&ctx.host_url_apub) {
        let rest = &query.resource[ctx.host_url_apub.len()..];
    let found_ref = if query.resource.starts_with(&ctx.host_url_apub.as_str()) {
        let rest = &query.resource[ctx.host_url_apub.as_str().len()..];
        if rest.starts_with("/users/") {
            if let Ok(id) = rest[7..].parse() {
                Some(LocalRef::UserID(id))


@@ 147,14 147,15 @@ async fn handler_webfinger_get(
                    crate::apub_util::get_local_community_apub_id(id, &ctx.host_url_apub)
                }
            };
            let alias = alias.as_str();

            let body = FingerResponse {
                subject: subject.into(),
                aliases: vec![(&alias).into()],
                aliases: vec![alias.into()],
                links: vec![FingerLink {
                    rel: "self".into(),
                    type_: Some(crate::apub_util::ACTIVITY_TYPE.into()),
                    href: Some((&alias).into()),
                    href: Some(alias.into()),
                }],
            };


M src/tasks.rs => src/tasks.rs +5 -5
@@ 11,7 11,7 @@ pub trait TaskDef: Serialize + std::fmt::Debug + Sync {

#[derive(Deserialize, Serialize, Debug)]
pub struct DeliverToInbox<'a> {
    pub inbox: Cow<'a, str>,
    pub inbox: Cow<'a, url::Url>,
    pub sign_as: Option<crate::ActorLocalRef>,
    pub object: String,
}


@@ 23,7 23,7 @@ impl<'a> TaskDef for DeliverToInbox<'a> {
    async fn perform(self, ctx: &crate::BaseContext) -> Result<(), crate::Error> {
        let db = ctx.db_pool.get().await?;

        let signing_info = match self.sign_as {
        let signing_info: Option<(_, _)> = match self.sign_as {
            None => None,
            Some(actor_ref) => Some(
                crate::apub_util::fetch_or_create_local_actor_privkey(


@@ 35,7 35,7 @@ impl<'a> TaskDef for DeliverToInbox<'a> {
            ),
        };

        let mut req = hyper::Request::post(self.inbox.as_ref())
        let mut req = hyper::Request::post(self.inbox.as_str().parse::<hyper::Uri>()?)
            .header(hyper::header::CONTENT_TYPE, crate::apub_util::ACTIVITY_TYPE)
            .body(self.object.into())?;



@@ 45,7 45,7 @@ impl<'a> TaskDef for DeliverToInbox<'a> {

            if let Some((privkey, key_id)) = signing_info {
                let signature = hancock::Signature::create_legacy(
                    &key_id,
                    key_id.as_str(),
                    &hyper::Method::POST,
                    &path_and_query,
                    req.headers(),


@@ 94,7 94,7 @@ impl TaskDef for DeliverToFollowers {

#[derive(Deserialize, Serialize, Debug)]
pub struct FetchActor<'a> {
    pub actor_ap_id: Cow<'a, str>,
    pub actor_ap_id: Cow<'a, url::Url>,
}

#[async_trait]