apub-core 0.2.0

Utilities for building activitypub servers
Documentation
//! traits around accepting activities

use crate::{
    repo::{Dereference, Repo},
    session::Session,
};
use std::{rc::Rc, sync::Arc};
use url::{Host, Url};

/// The Authority that is providing the ingested data
///
/// - An Authority of None means the data is untrustworthy, as no entity can be verified to have
///    provided this data
/// - An Authority of Server means the data to be ingested has been provided on behalf of it's
///   origin server. A URL is provided in the Server variant to describe the specific URL the data
///   originates from.
/// - An Authority of Actor(Url) means the data to be ingested has been provided on behalf of the
///   Actor identified by the associated URL
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Authority {
    /// No Authority provided
    None,

    /// The Authority for ingested data is the server the data is hosted on
    Server(Url),

    /// The Authority for the ingested data is the provided url
    Actor(Url),
}

/// Describes accepting a new Object into the system
///
/// This type is implemented by users of `apub` to hook into provied inbox methods
#[async_trait::async_trait(?Send)]
pub trait Ingest<Object> {
    /// The actor that is receiving the activity
    ///
    /// e.g.
    /// - the community that is receiving a new post
    /// - the server actor (in the case of the shared inbox)
    type ActorId;

    /// The local repository
    type Local: Repo;

    /// The error produced when ingesting activities
    type Error: From<<Self::Local as Repo>::Error>;

    /// Get the local repository
    fn local_repo(&self) -> &Self::Local;

    /// Determine if a given URL is local
    fn is_local(&self, url: &Url) -> bool;

    /// Accept and process a given activity
    ///
    /// Args:
    /// - authority: the source of the information, either the Actor that provided the object, or the URL it was fetched from
    /// - actor_id: the ID of the actor accepting the object.
    /// - activity: the Object being ingested
    /// - remote_repo: a handle to a remote repository (probably an HTTP client) for ingesting further objects
    /// - session: the request session associated with the Remote Repo
    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>;

    /// Dereference an ID from the local or remote repo
    ///
    /// Args:
    /// - id: the ID of the object to be fetched
    /// - actor_id: the ID of the actor fetching the object.
    /// - remote_repo: a handle to a remote repository (probably an HTTP client) for ingesting further objects
    /// - session: the request session associated with the Remote Repo
    async fn fetch<D: Dereference<Output = Object>, Remote: Repo, S: Session>(
        &self,
        id: D,
        actor_id: Self::ActorId,
        remote_repo: Remote,
        mut session: S,
    ) -> Result<Option<D::Output>, Self::Error>
    where
        Self::ActorId: 'static,
        Self::Error: From<Remote::Error>,
    {
        let opt = self.local_repo().fetch(&id, &mut session).await?;

        if self.is_local(id.url()) {
            return Ok(opt);
        }

        let opt = remote_repo.fetch(&id, &mut session).await?;

        if let Some(object) = opt.as_ref() {
            let authority = Authority::Server(id.url().clone());
            self.ingest(authority, actor_id, object, remote_repo, session)
                .await?;
        }

        Ok(opt)
    }
}

/// Describes a type that can produce an Ingest
pub trait IngestFactory<A> {
    /// The Ingest type
    type Ingest: Ingest<A>;

    /// Build the ingest type
    fn build_ingest(&self) -> Self::Ingest;
}

impl std::fmt::Display for Authority {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Authority::Actor(actor_id) => write!(f, "Actor - '{}'", actor_id),
            Authority::Server(url) => write!(f, "Server URL - '{}'", url),
            Authority::None => write!(f, "None"),
        }
    }
}

/// A helper for determining if a given URL matches a Host and Port
pub fn is_local(local_host: &Host<String>, local_port: Option<u16>, url: &Url) -> bool {
    Some(borrow_host(local_host)) == url.host() && local_port == url.port()
}

fn borrow_host(host: &Host<String>) -> Host<&str> {
    match host {
        Host::Ipv4(ip) => Host::Ipv4(*ip),
        Host::Ipv6(ip) => Host::Ipv6(*ip),
        Host::Domain(ref domain) => Host::Domain(domain),
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, Object, T> Ingest<Object> for &'a T
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Local = T::Local;
    type ActorId = T::ActorId;
    type Error = T::Error;

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

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

    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>,
    {
        T::ingest(self, authority, actor_id, activity, remote_repo, session).await
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, Object, T> Ingest<Object> for &'a mut T
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Local = T::Local;
    type Error = T::Error;
    type ActorId = T::ActorId;

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

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

    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>,
    {
        T::ingest(self, authority, actor_id, activity, remote_repo, session).await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Box<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Local = T::Local;
    type Error = T::Error;
    type ActorId = T::ActorId;

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

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

    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>,
    {
        T::ingest(self, authority, actor_id, activity, remote_repo, session).await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Rc<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Local = T::Local;
    type Error = T::Error;
    type ActorId = T::ActorId;

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

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

    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>,
    {
        T::ingest(self, authority, actor_id, activity, remote_repo, session).await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Arc<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Local = T::Local;
    type Error = T::Error;
    type ActorId = T::ActorId;

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

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

    async fn ingest<Remote: Repo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: &Object,
        remote_repo: Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self::Error: From<Remote::Error>,
    {
        T::ingest(self, authority, actor_id, activity, remote_repo, session).await
    }
}