apub-ingest 0.2.0

Utilities for building activitypub servers
Documentation
use apub_core::{
    activitypub::{Activity, DeliverableObject},
    ingest::{Authority, Ingest},
    repo::Repo,
    session::Session,
};
use url::Url;

pub trait InboxType {
    fn is_shared(&self) -> bool;
}

/// Rejects if the Authority does not have permission to act on behalf of Actor
#[derive(Clone, Debug)]
pub struct ValidateAuthority<I> {
    ingest: I,
}

/// Rejects if a public message was not sent to a shared inbox
/// Rejects if a private message was sent to a shared inbox
#[derive(Clone, Debug)]
pub struct ValidateInbox<I> {
    ingest: I,
}

/// Rejects if Activity's host does not match Actor's host
#[derive(Clone, Debug)]
pub struct ValidateHosts<I> {
    ingest: I,
}

/// Rejects if the Authority does not have permission to act on behalf of Actor
pub fn validate_authority<I>(ingest: I) -> ValidateAuthority<I> {
    ValidateAuthority { ingest }
}

/// Rejects if a public message was not sent to a shared inbox
/// Rejects if a private message was sent to a shared inbox
pub fn validate_inbox<I>(ingest: I) -> ValidateInbox<I> {
    ValidateInbox { ingest }
}

/// Rejects if Activity's host does not match Actor's host
pub fn validate_hosts<I>(ingest: I) -> ValidateHosts<I> {
    ValidateHosts { ingest }
}

#[derive(Clone, Debug)]
pub struct AuthorityError {
    authority: Authority,
    actor: Url,
}

#[derive(Clone, Debug)]
pub struct InboxError;

#[derive(Clone, Debug)]
pub struct HostError;

#[async_trait::async_trait(?Send)]
impl<A, I> Ingest<A> for ValidateAuthority<I>
where
    A: Activity + 'static,
    I: Ingest<A>,
    I::Error: From<AuthorityError>,
{
    type Local = I::Local;
    type ActorId = I::ActorId;
    type Error = I::Error;

    fn local_repo(&self) -> &Self::Local {
        self.ingest.local_repo()
    }

    fn is_local(&self, url: &Url) -> bool {
        self.ingest.is_local(url)
    }

    async fn ingest<R: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &A,
        remote_repo: R,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<R::Error>,
    {
        let activity_actor = activity.actor_id();

        let valid = match &authority {
            Authority::Server(url) => {
                url.host() == activity_actor.host() && url.port() == activity_actor.port()
            }
            Authority::Actor(actor) => actor == activity_actor,
            Authority::None => false,
        };

        if !valid {
            return Err(I::Error::from(AuthorityError {
                authority,
                actor: activity_actor.clone(),
            }));
        }

        self.ingest
            .ingest(authority, actor_id, activity, remote_repo, session)
            .await
    }
}

#[async_trait::async_trait(?Send)]
impl<A, I> Ingest<A> for ValidateInbox<I>
where
    A: DeliverableObject + 'static,
    I: Ingest<A>,
    I::Error: From<InboxError>,
    I::ActorId: InboxType,
{
    type Local = I::Local;
    type ActorId = I::ActorId;
    type Error = I::Error;

    fn local_repo(&self) -> &Self::Local {
        self.ingest.local_repo()
    }

    fn is_local(&self, url: &Url) -> bool {
        self.ingest.is_local(url)
    }

    async fn ingest<R: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &A,
        remote_repo: R,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<R::Error>,
    {
        // delivered a public activity to a user inbox
        if !actor_id.is_shared() && activity.is_public() {
            return Err(I::Error::from(InboxError));
        }

        // delivered a private activity to a shared inbox
        if actor_id.is_shared() && !activity.is_public() {
            return Err(I::Error::from(InboxError));
        }

        self.ingest
            .ingest(authority, actor_id, activity, remote_repo, session)
            .await
    }
}

#[async_trait::async_trait(?Send)]
impl<A, I> Ingest<A> for ValidateHosts<I>
where
    A: Activity + 'static,
    I: Ingest<A>,
    I::Error: From<HostError>,
{
    type Local = I::Local;
    type ActorId = I::ActorId;
    type Error = I::Error;

    fn local_repo(&self) -> &Self::Local {
        self.ingest.local_repo()
    }

    fn is_local(&self, url: &Url) -> bool {
        self.ingest.is_local(url)
    }
    async fn ingest<R: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &A,
        remote_repo: R,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<R::Error>,
    {
        if activity.id().host() != activity.actor_id().host()
            || activity.id().port() != activity.actor_id().port()
        {
            return Err(I::Error::from(HostError));
        }

        self.ingest
            .ingest(authority, actor_id, activity, remote_repo, session)
            .await
    }
}

impl std::fmt::Display for AuthorityError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Authority '{}' not permitted to act on behalf of actor '{}'",
            self.authority, self.actor
        )
    }
}
impl std::error::Error for AuthorityError {}

impl std::fmt::Display for InboxError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Object delivered to invalid inbox")
    }
}
impl std::error::Error for InboxError {}

impl std::fmt::Display for HostError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Activity and Actor host do not match")
    }
}
impl std::error::Error for HostError {}