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 / Vervis / Handler /

Person.hs

{- This file is part of Vervis.
 -
 - Written in 2016, 2018, 2019, 2022, 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/>.
 -}

module Vervis.Handler.Person
    ( getPersonR
    , getPersonInboxR
    , postPersonInboxR
    , getPersonOutboxR
    , postPersonOutboxR
    , getPersonOutboxItemR
    , getPersonFollowersR
    , getPersonFollowingR
    , getSshKeyR
    , getPersonMessageR

    , postPersonFollowR
    , postPersonUnfollowR

    , getPersonStampR
    )
where

import Control.Monad
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe
import Data.Text (Text)
import Data.Time.Clock
import Data.Traversable
import Database.Persist
import Database.Persist.Sql
import Dvara
import Text.Blaze.Html (toHtml)
import Yesod.Core
import Yesod.Auth.Account (newAccountR, resendVerifyEmailWidget, username)
import Yesod.Auth.Account.Message (AccountMsg (MsgEmailUnverified))
import Yesod.Persist.Core

import qualified Data.Text as T (unpack)

import Yesod.Auth.Unverified (requireUnverifiedAuth)

import Text.Email.Local

import Network.FedURI
import Yesod.ActivityPub
import Yesod.Actor
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite

import qualified Web.ActivityPub as AP

import Control.Monad.Trans.Except.Local
import Data.Either.Local
import Database.Persist.Local

import Vervis.ActivityPub
import Vervis.API
import Vervis.Data.Actor
import Vervis.Federation.Auth
import Vervis.Federation.Collab
import Vervis.Federation.Discussion
import Vervis.Federation.Offer
import Vervis.FedURI
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Persist.Actor
import Vervis.Recipient
import Vervis.Secure
import Vervis.Settings
import Vervis.Ticket
import Vervis.Web.Actor
import Vervis.Web.Discussion
import Vervis.Widget
import Vervis.Widget.Person

getPersonR :: KeyHashid Person -> Handler TypedContent
getPersonR personHash = do
    personID <- decodeKeyHashid404 personHash
    (person, actor, sigKeyIDs, sshKeyIDs) <- runDB $ do
        p <- get404 personID
        let aid = personActor p
        a <- getJust aid
        sigKeys <- selectKeysList [SigKeyActor ==. aid] [Asc SigKeyId]
        sshKeys <- selectKeysList [SshKeyPerson ==. personID] [Asc SshKeyId]
        return (p, a, sigKeys, sshKeys)
    encodeRouteLocal <- getEncodeRouteLocal
    hashSigKey <- getEncodeKeyHashid
    hashSshKey <- getEncodeKeyHashid
    perActor <- asksSite $ appPerActorKeys . appSettings

    let personAP = AP.Actor
            { AP.actorLocal = AP.ActorLocal
                { AP.actorId         = encodeRouteLocal $ PersonR personHash
                , AP.actorInbox      = encodeRouteLocal $ PersonInboxR personHash
                , AP.actorOutbox     = Just $ encodeRouteLocal $ PersonOutboxR personHash
                , AP.actorFollowers  = Just $ encodeRouteLocal $ PersonFollowersR personHash
                , AP.actorFollowing  = Just $ encodeRouteLocal $ PersonFollowingR personHash
                , AP.actorPublicKeys =
                    map (Left . encodeRouteLocal) $
                    if perActor
                        then map (PersonStampR personHash . hashSigKey) sigKeyIDs
                        else [ActorKey1R, ActorKey2R]
                , AP.actorSshKeys    =
                    map (encodeRouteLocal . SshKeyR personHash . hashSshKey) sshKeyIDs
                }
            , AP.actorDetail = AP.ActorDetail
                { AP.actorType       = AP.ActorTypePerson
                , AP.actorUsername   = Just $ username2text $ personUsername person
                , AP.actorName       = Just $ actorName actor
                , AP.actorSummary    = Just $ actorDesc actor
                }
            }
        followButton =
            followW
                (PersonFollowR personHash)
                (PersonUnfollowR personHash)
                (actorFollowers actor)

    let ep = Entity personID person
    secure <- getSecure
    provideHtmlAndAP personAP $(widgetFile "person")

getPersonInboxR :: KeyHashid Person -> Handler TypedContent
getPersonInboxR = getInbox PersonInboxR personActor

postPersonInboxR :: KeyHashid Person -> Handler ()
postPersonInboxR personHash = do
    personID <- decodeKeyHashid404 personHash
    postInbox $ LocalActorPerson personID

getPersonOutboxR :: KeyHashid Person -> Handler TypedContent
getPersonOutboxR = getOutbox PersonOutboxR PersonOutboxItemR personActor

postPersonOutboxR :: KeyHashid Person -> Handler TypedContent
postPersonOutboxR personHash = do
    federation <- getsYesod $ appFederation . appSettings
    unless federation badMethod

    personID <- decodeKeyHashid404 personHash
    (person, actor) <- runDB $ do
        p <- get404 personID
        (p,) <$> getJust (personActor p)

    verifyPermission personID
    verifyContentTypeAP

    AP.Doc h activity <- requireInsecureJsonBody
    hl <- hostIsLocalOld h
    unless hl $ invalidArgs ["Activity host isn't the instance host"]

    result <- runExceptT $ do
        verifyAttribution $ AP.activityActor activity
        unless (null $ AP.activityFulfills activity) $
            throwE "Specifying 'fulfills' manually isn't allowed currently"
        handle (Entity personID person) actor activity
    case result of
        Left err -> invalidArgs [err]
        Right outboxItemID -> do
            outboxItemHash <- encodeKeyHashid outboxItemID
            sendResponseCreated $ PersonOutboxItemR personHash outboxItemHash
    where
    verifyPermission recipientID = do
        (_app, mpid, _scopes) <- maybe notAuthenticated return =<< getDvaraAuth
        senderID <-
            maybe (permissionDenied "Not authorized to post as a user") return mpid
        unless (recipientID == senderID) $
            permissionDenied "Can't post as other users"

    verifyAttribution actor =
        case decodeRouteLocal actor of
            Just (PersonR actorHash) | actorHash == personHash -> return ()
            _ -> throwE "Can't post activity attributed to someone else"

    checkFederation remoteRecips = do
        federation <- asksSite $ appFederation . appSettings
        unless (federation || null remoteRecips) $
            throwE "Federation disabled, but remote recipients found"

    handle eperson actorDB (AP.Activity _mid _actorAP muCap summary audience _fulfills specific) = do
        maybeCap <- traverse (nameExceptT "Capability" . parseActivityURI) muCap
        ParsedAudience localRecips remoteRecips blinded fwdHosts <- do
            mrecips <- parseAudience audience
            fromMaybeE mrecips "No recipients"
        checkFederation remoteRecips
        let action = AP.Action
                { AP.actionCapability = muCap
                , AP.actionSummary    = summary
                , AP.actionAudience   = blinded
                , AP.actionFulfills   = []
                , AP.actionSpecific   = specific
                }
            run :: (   Entity Person
                    -> Actor
                    -> Maybe
                        ( Either
                            (LocalActorBy Key, LocalActorBy KeyHashid, OutboxItemId)
                            FedURI
                        )
                    -> RecipientRoutes
                    -> [(Host, NonEmpty LocalURI)]
                    -> [Host]
                    -> AP.Action URIMode
                    -> t
                   )
                -> t
            run f = f eperson actorDB maybeCap localRecips remoteRecips fwdHosts action
        case specific of
            AP.AcceptActivity accept -> run acceptC accept
            AP.ApplyActivity apply -> run applyC apply
            AP.CreateActivity (AP.Create obj mtarget) ->
                case obj of
                    AP.CreateNote _ note ->
                        run createNoteC note mtarget
                    AP.CreateTicketTracker detail mlocal ->
                        run createTicketTrackerC detail mlocal mtarget
                    AP.CreateRepository detail vcs mlocal ->
                        run createRepositoryC detail vcs mlocal mtarget
                    AP.CreatePatchTracker detail repos mlocal ->
                        run createPatchTrackerC detail repos mlocal mtarget
                    _ -> throwE "Unsupported Create 'object' type"
            AP.InviteActivity invite -> run inviteC invite
            {-
            AddActivity (AP.Add obj target) ->
                case obj of
                    Right (AddBundle patches) ->
                        addBundleC eperson sharer summary audience patches target
                    _ -> throwE "Unsupported Add 'object' type"
            -}
            AP.FollowActivity follow -> run followC follow
            AP.OfferActivity (AP.Offer obj target) ->
                case obj of
                    AP.OfferTicket ticket -> run offerTicketC ticket target
                    {-
                    OfferDep dep ->
                        offerDepC eperson sharer summary audience dep target
                    -}
                    _ -> throwE "Unsupported Offer 'object' type"
            AP.ResolveActivity resolve -> run resolveC resolve
            AP.UndoActivity undo -> run undoC undo
            _ -> throwE "Unsupported activity type"

getPersonOutboxItemR
    :: KeyHashid Person -> KeyHashid OutboxItem -> Handler TypedContent
getPersonOutboxItemR = getOutboxItem PersonOutboxItemR personActor

getPersonFollowersR :: KeyHashid Person -> Handler TypedContent
getPersonFollowersR = getActorFollowersCollection PersonFollowersR personActor

getPersonFollowingR :: KeyHashid Person -> Handler TypedContent
getPersonFollowingR = getFollowingCollection PersonFollowingR personActor

getSshKeyR :: KeyHashid Person -> KeyHashid SshKey -> Handler TypedContent
getSshKeyR personHash keyHash = do
    personID <- decodeKeyHashid404 personHash
    keyID <- decodeKeyHashid404 keyHash
    key <- runDB $ do
        _ <- get404 personID
        k <- get404 keyID
        unless (sshKeyPerson k == personID) notFound
        return k

    encodeRouteLocal <- getEncodeRouteLocal
    let here = SshKeyR personHash keyHash
        keyAP = AP.SshPublicKey
            { AP.sshPublicKeyId        = encodeRouteLocal here
            , AP.sshPublicKeyExpires   = Nothing
            , AP.sshPublicKeyOwner     = encodeRouteLocal $ PersonR personHash
            , AP.sshPublicKeyAlgorithm =
                case sshKeyAlgo key of
                    "ssh-rsa" -> AP.SshKeyAlgorithmRSA
                    _         -> error "Unexpected sshKeyAlgo in DB"
            , AP.sshPublicKeyMaterial  = sshKeyContent key
            }
    provideHtmlAndAP keyAP $ redirectToPrettyJSON here

getPersonMessageR
    :: KeyHashid Person -> KeyHashid LocalMessage -> Handler TypedContent
getPersonMessageR personHash localMessageHash =
    serveMessage personHash localMessageHash

postPersonFollowR :: KeyHashid Person -> Handler ()
postPersonFollowR _ = error "Temporarily disabled"

postPersonUnfollowR :: KeyHashid Person -> Handler ()
postPersonUnfollowR _ = error "Temporarily disabled"

getPersonStampR :: KeyHashid Person -> KeyHashid SigKey -> Handler TypedContent
getPersonStampR = servePerActorKey personActor LocalActorPerson
[See repo JSON]