{-# LANGUAGE DisambiguateRecordFields #-}
module Puppy.Protocol.ActivityPub (
module Puppy.Protocol.ActivityStreams,
ActivityPub,
runActivityPub,
dereference,
perform,
deliver,
genId,
doDeliverTask,
doPerformTask,
) where
import Data.Functor ((<&>))
import Effectful
import Effectful.Concurrent.Async (forConcurrently_, Concurrent)
import Effectful.Dispatch.Dynamic
import Effectful.Fail (Fail, runFail)
import Puppy.Context
import Puppy.Crypto.RNG (RNG, genBytes)
import Puppy.Database (DB)
import Puppy.Logging
import Puppy.Protocol.ActivityStreams
import Puppy.Protocol.HTTP (HTTP, post, get, Response (..), Sign)
import Puppy.TaskQueue
import Puppy.Types (Channel (..), ChannelSettings (..))
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64.URL as BS
import qualified Puppy.Database as DB
import qualified Puppy.Protocol.ActivityPub.Fetch as F
data ActivityPub :: Effect where
Resolve :: forall obj m. (F.Dereference obj) => Either JSON.Value Id -> ActivityPub m obj
Perform :: Activity Object -> Channel -> ActivityPub m ()
Deliver :: Activity Object -> Channel -> ActivityPub m ()
type instance DispatchOf ActivityPub = 'Dynamic
resolve :: (ActivityPub :> es, F.Dereference obj) => Id -> Eff es obj
resolve = send . Resolve . Right
dereference :: (ActivityPub :> es, F.Dereference obj) => JSON.Value -> Eff es obj
dereference = send . Resolve . Left
perform :: (ActivityPub :> es) => Activity Object -> Channel -> Eff es ()
perform activity channel = send (Perform activity channel)
deliver :: (ActivityPub :> es) => Activity Object -> Channel -> Eff es ()
deliver activity channel = send (Deliver activity channel)
genId :: (ServerInfo :> es, RNG :> es) => Eff es Id
genId = do
suffix <- BS.encodeBase64 <$> genBytes 32
localId ("/ap/o/" <> suffix)
runActivityPub
:: (
TaskQueue DeliverTask :> es,
TaskQueue PerformTask :> es,
Fail :> es,
HTTP :> es,
Sign :> es,
Log :> es
) => Eff (ActivityPub : es) a
-> Eff es a
runActivityPub = interpret $ \_ -> \case
Resolve (Left json) -> F.runFetch json F.dereferencer
Resolve (Right (Id url)) -> do
json <- get url >>= parseJSON
F.runFetch json F.dereferencer
Perform activity channel -> enqueue (PerformTask activity channel)
Deliver activity channel -> enqueue (DeliverTask activity channel)
where
parseJSON :: (Fail :> es) => Response -> Eff es JSON.Value
parseJSON (Response { body }) =
case JSON.decode (BS.fromStrict body) of
Just value -> pure value
Nothing -> fail "Couldn't decode body"
doDeliverTask
:: (
Concurrent :> es,
HTTP :> es,
Sign :> es,
Log :> es
) => Activity Object
-> Eff es ()
doDeliverTask activity = scope "deliver" $ do
let payload = preparePayload activity
forConcurrently_ (activity.to <> activity.cc) $ \(Inbox inbox) -> do
debug ("Delivering to: " <> inbox)
runFail (post payload inbox) >>= \case
Left _ -> warn ("Failed to deliver to " <> inbox)
Right resp
| resp.statusCode > 299 -> warn "Encountered non-200 status code"
| otherwise -> debug "Delivery OK"
where
preparePayload :: Activity Object -> JSON.Value
preparePayload aty = JSON.toJSON $ aty <&> (.id)
doPerformTask
:: (
ActivityPub :> es,
Log :> es,
DB :> es,
Fail :> es,
RNG :> es,
ServerInfo :> es
) => Activity Object
-> Eff es ()
doPerformTask a = scope "perform" $ do
DB.storeActivity a
case a.subtype of
Follow | Just (followee :: Actor) <- fromObject a.object
-> do
DB.insertFollowRequest (a.actor, followee.id)
DB.getChannelByActorId followee.id >>= \case
Just channel | channel.settings.autoAcceptFollows -> acceptFollow channel
Just _ -> debug "Not automatically accepting follow request due to channel settings"
Nothing -> debug "Follow request does not target a channel, not doing anything"
Accept | Just o@Activity { subtype = Follow } <- fromObject a.object,
Just (followee :: Actor) <- fromObject o.object
-> do
debug ("Accepting follow request: " <> o.actor.url <> " to " <> followee.id.url)
DB.acceptFollowRequest (o.actor, followee.id)
Undo | Just o@Activity { subtype = Follow } <- fromObject a.object,
Just (followee :: Actor) <- fromObject o.object
-> do
debug "Cancelling follow request"
DB.cancelFollowRequest (o.actor, followee.id)
ty -> do
fail ("Combination of activity type " <> show ty <> " and object type unknown")
where
acceptFollow channel = do
debug "Automatically accepting follow request for channel"
reply <- do
activityId <- genId
follower :: Actor <- resolve a.actor
target <- case follower.inbox of
Just inbox -> pure inbox
Nothing -> fail "No inbox available for follow requester"
pure (Activity {
id = activityId,
actor = Id channel.linkedActorId,
object = toObject a,
to = [target],
cc = [],
time = Nothing,
subtype = Accept
})
deliver reply channel
perform reply channel