Eventually-decentralized project hosting and management platform

[[ 🗃 ^WvWbo vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Changes]

Clone

HTTPS: darcs clone https://vervis.peers.community/repos/WvWbo

SSH: darcs clone USERNAME@vervis.peers.community:WvWbo

Tags

TODO

src / Web / Actor /

Deliver.hs

{- This file is part of Vervis.
 -
 - Written in 2023 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}

-- | Should eventually turn into an internal module for use only by
-- 'Web.Actor'.
--
-- System of local utility-actors that do the actual HTTP POSTing of
-- activities to remote actors.
module Web.Actor.Deliver
    ( Method (..)
    , DeliveryTheater ()
    , startDeliveryTheater
    , sendHttp
    )
where

import Control.Exception.Base
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Retry
import Data.ByteString (ByteString)
import Data.Foldable
import Data.Hashable
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import Data.Time.Clock
import Data.Time.Interval
import Data.Traversable
import Database.Persist.Sql
import Network.HTTP.Client (Manager)
import Network.HTTP.Types.Header (HeaderName)
import System.Directory
import Web.Hashids

import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashSet as HS
import qualified Data.Text as T

import Control.Concurrent.Actor
import Database.Persist.Box
import Network.FedURI

import qualified Web.ActivityPub as AP

import Vervis.Settings

data Method u
    = MethodDeliverLocal (AP.Envelope u) Bool
    | MethodForwardRemote (AP.Errand u)

instance Message (Method u) where
    summarize _ = "Method"
    refer _     = "Method"

data RemoteActor = RemoteActor
    { raInbox      :: Maybe LocalURI
    , _raErrorSince :: Maybe UTCTime
    }
    deriving (Show, Read)

instance BoxableVia RemoteActor where
    type BV RemoteActor = BoxableShow

{-
migrations :: [Migration SqlBackend IO]
migrations =
    [ -- 1
      addEntities [entities|
        RemoteActor
            inbox      LocalURI Maybe
            errorSince UTCTime  Maybe
        |]
    ]
-}

data Env u = Env
    { envBox :: Box RemoteActor
    }

instance MonadBox (ActFor (Env u)) where
    type BoxType (ActFor (Env u)) = RemoteActor
    askBox = asksEnv envBox

instance Stage (Env u) where
    type StageKey (Env u)     = ObjURI u
    type StageMessage (Env u) = Method u
    type StageReturn (Env u)  = ()

data DeliveryTheater u = DeliveryTheater
    { _dtManager :: Manager
    , _dtHeaders :: NonEmpty HeaderName
    , _dtDelay   :: Int
    , _dtLog     :: LogFunc
    , _dtTheater :: TheaterFor (Env u)
    }

data IdMismatch = IdMismatch deriving Show

instance Exception IdMismatch

behavior
    :: UriMode u
    => Manager
    -> NonEmpty HeaderName
    -> Int
    -> ObjURI u
    -> Method u
    -> ActFor (Env u) ((), ActFor (Env u) (), Next)
behavior manager postSignedHeaders micros (ObjURI h lu) = \case
    MethodDeliverLocal envelope fwd -> do
        ra@(RemoteActor mluInbox _mError) <- runBox obtain
        uInbox <- getInbox
        let mluFwd = if fwd then Just lu else Nothing
        _resp <-
            liftIO $ retry toException $
                AP.deliver manager postSignedHeaders envelope mluFwd uInbox
        done ()
    MethodForwardRemote errand -> do
        uInbox <- getInbox
        _resp <-
            liftIO $ retry toException $
                AP.forward manager postSignedHeaders errand uInbox
        done ()
    where
    retry :: (e -> SomeException) -> IO (Either e a) -> IO a
    retry toE action = do
        errorOrResult <-
            runExceptT $
                retryOnError
                    (exponentialBackoff micros)
                    (\ _ _ -> pure True)
                    (const $ ExceptT action)
        case errorOrResult of
            Left e -> throwIO $ toE e
            Right r -> return r
    getInbox = do
        ra@(RemoteActor mluInbox _mError) <- runBox obtain
        luInbox <-
            case mluInbox of
                Just luInb -> return luInb
                Nothing -> do
                    AP.Actor local _detail <-
                        liftIO $
                            retry
                                (maybe (toException IdMismatch) toException)
                                (AP.fetchAPID' manager (AP.actorId . AP.actorLocal) h lu)
                    let luInb = AP.actorInbox local
                    runBox $ bestow $ ra { raInbox = Just luInb }
                    return luInb
        return $ ObjURI h luInbox

mkEnv :: LogFunc -> OsPath -> IO (Env u)
mkEnv logFunc path = flip runLoggingT logFunc $ do
    box <- loadBox {-migrations-} path (RemoteActor Nothing Nothing)
    return $ Env box

type OsPath = FilePath
encodeUtf = pure
decodeUtf = pure

startDeliveryTheater
    :: UriMode u
    => NonEmpty HeaderName
    -> Int
    -> Manager
    -> LogFunc
    -> OsPath
    -> IO (DeliveryTheater u)
startDeliveryTheater headers micros manager logFunc dbRootDir = do
    entries <- listDirectory dbRootDir
    actors <- for entries $ \ path -> do
        path' <- T.pack <$> decodeUtf path
        u <-
            case parseObjURI path' of
                Left e ->
                    error $
                        "Failed to parse URI-named SQLite db filename: " ++ e
                Right uri -> return uri
        env <- mkEnv logFunc path
        return (u, env, behavior manager headers micros u)
    DeliveryTheater manager headers micros logFunc <$> startTheater logFunc actors

sendHttp :: UriMode u => DeliveryTheater u -> Method u -> [ObjURI u] -> IO ()
sendHttp (DeliveryTheater manager headers micros logFunc theater) method recips = do
    for_ recips $ \ u ->
        let makeEnv = encodeUtf (T.unpack $ renderObjURI u) >>= mkEnv logFunc
            behave = behavior manager headers micros u
        in  void $ spawnIO theater u makeEnv behave
    sendManyIO theater (HS.fromList recips) method
[See repo JSON]