1
0
Fork 0
mirror of https://code.naskya.net/repos/ndqEd synced 2025-01-30 05:07:51 +09:00
vervis/src/Vervis/ActivityPub.hs

952 lines
40 KiB
Haskell

{- This file is part of Vervis.
-
- Written in 2019, 2020 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.ActivityPub
( hostIsLocal
, verifyHostLocal
, parseContext
, parseParent
, runDBExcept
, getLocalParentMessageId
, getPersonOrGroupId
, getTicketTeam
, getProjectTeam
, getRepoTeam
, getFollowers
, unionRemotes
, insertMany'
, isInstanceErrorP
, isInstanceErrorG
, deliverHttp
, deliverHttpBL
, deliverRemoteDB
, deliverRemoteHTTP
, checkForward
, parseTarget
--, checkDep
, getProjectAndDeps
, deliverRemoteDB'
, deliverRemoteHttp
, serveCommit
, deliverLocal
, RemoteRecipient (..)
, deliverLocal'
)
where
import Control.Applicative
import Control.Exception hiding (Handler, try)
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader
import Data.Bifunctor
import Data.ByteString (ByteString)
import Data.Either
import Data.Foldable
import Data.Function
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import Data.Maybe
import Data.Semigroup
import Data.Text (Text)
import Data.Text.Encoding
import Data.Time.Clock
import Data.Traversable
import Database.Persist
import Database.Persist.Sql
import Network.HTTP.Client
import Network.TLS -- hiding (SHA256)
import Text.Blaze.Html (preEscapedToHtml)
import Text.Blaze.Html.Renderer.Text
import UnliftIO.Exception (try)
import Yesod.Core hiding (logError, logWarn, logInfo, logDebug)
import Yesod.Core.Handler
import Yesod.Persist.Core
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.List.NonEmpty as NE
import qualified Data.List as L
import qualified Data.List.Ordered as LO
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Database.Esqueleto as E
import Yesod.HttpSignature
import Database.Persist.JSON
import Network.FedURI
import Network.HTTP.Digest
import Web.ActivityPub hiding (Author (..), Ticket)
import Yesod.ActivityPub
import Yesod.MonadSite
import Yesod.FedURI
import Yesod.Hashids
import qualified Web.ActivityPub as AP
import Control.Monad.Trans.Except.Local
import Data.Either.Local
import Data.List.NonEmpty.Local
import Data.Tuple.Local
import Database.Persist.Local
import Vervis.ActivityPub.Recipient
import Vervis.FedURI
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Patch
import Vervis.RemoteActorStore
import Vervis.Settings
import Vervis.Time
import Vervis.Widget.Repo
import Vervis.Widget.Sharer
hostIsLocal :: (MonadSite m, SiteEnv m ~ App) => Host -> m Bool
hostIsLocal h = asksSite $ (== h) . appInstanceHost . appSettings
verifyHostLocal
:: (MonadSite m, SiteEnv m ~ App)
=> Host -> Text -> ExceptT Text m ()
verifyHostLocal h t = do
local <- hostIsLocal h
unless local $ throwE t
parseContext
:: (MonadSite m, SiteEnv m ~ App)
=> FedURI
-> ExceptT Text m (Either (ShrIdent, PrjIdent, KeyHashid LocalTicket) FedURI)
parseContext uContext = do
let ObjURI hContext luContext = uContext
local <- hostIsLocal hContext
if local
then Left <$> do
route <- case decodeRouteLocal luContext of
Nothing -> throwE "Local context isn't a valid route"
Just r -> return r
case route of
ProjectTicketR shr prj num -> return (shr, prj, num)
_ -> throwE "Local context isn't a ticket route"
else return $ Right uContext
parseParent
:: (MonadSite m, SiteEnv m ~ App)
=> FedURI
-> ExceptT Text m (Either (ShrIdent, LocalMessageId) FedURI)
parseParent uParent = do
let ObjURI hParent luParent = uParent
local <- hostIsLocal hParent
if local
then Left <$> do
route <- case decodeRouteLocal luParent of
Nothing -> throwE "Local parent isn't a valid route"
Just r -> return r
case route of
MessageR shr lmkhid ->
(shr,) <$>
decodeKeyHashidE lmkhid
"Local parent has non-existent message \
\hashid"
_ -> throwE "Local parent isn't a message route"
else return $ Right uParent
newtype FedError = FedError Text deriving Show
instance Exception FedError
runDBExcept :: (MonadUnliftIO m, MonadSite m, SiteEnv m ~ App) => ExceptT Text (ReaderT SqlBackend m) a -> ExceptT Text m a
runDBExcept action = do
result <-
lift $ try $ runSiteDB $ either abort return =<< runExceptT action
case result of
Left (FedError t) -> throwE t
Right r -> return r
where
abort = liftIO . throwIO . FedError
getLocalParentMessageId :: DiscussionId -> ShrIdent -> LocalMessageId -> ExceptT Text AppDB MessageId
getLocalParentMessageId did shr lmid = do
mlm <- lift $ get lmid
lm <- fromMaybeE mlm "Local parent: no such lmid"
p <- lift $ getJust $ localMessageAuthor lm
s <- lift $ getJust $ personIdent p
unless (shr == sharerIdent s) $ throwE "Local parent: No such message, lmid mismatches sharer"
let mid = localMessageRest lm
m <- lift $ getJust mid
unless (messageRoot m == did) $
throwE "Local parent belongs to a different discussion"
return mid
getPersonOrGroupId :: SharerId -> AppDB (Either PersonId GroupId)
getPersonOrGroupId sid = do
mpid <- getKeyBy $ UniquePersonIdent sid
mgid <- getKeyBy $ UniqueGroup sid
requireEitherM mpid mgid
"Found sharer that is neither person nor group"
"Found sharer that is both person and group"
getTicketTeam :: SharerId -> AppDB ([PersonId], [((InstanceId, Host), NonEmpty RemoteRecipient)])
getTicketTeam sid = do
id_ <- getPersonOrGroupId sid
(,[]) <$> case id_ of
Left pid -> return [pid]
Right gid ->
map (groupMemberPerson . entityVal) <$>
selectList [GroupMemberGroup ==. gid] [Asc GroupMemberPerson]
getProjectTeam = getTicketTeam
getRepoTeam = getTicketTeam
getFollowers :: FollowerSetId -> AppDB ([PersonId], [((InstanceId, Host), NonEmpty RemoteRecipient)])
getFollowers fsid = do
local <- selectList [FollowTarget ==. fsid] [Asc FollowPerson]
remote <- E.select $ E.from $ \ (rf `E.InnerJoin` ra `E.InnerJoin` ro `E.InnerJoin` i) -> do
E.on $ ro E.^. RemoteObjectInstance E.==. i E.^. InstanceId
E.on $ ra E.^. RemoteActorIdent E.==. ro E.^. RemoteObjectId
E.on $ rf E.^. RemoteFollowActor E.==. ra E.^. RemoteActorId
E.where_ $ rf E.^. RemoteFollowTarget E.==. E.val fsid
E.orderBy [E.asc $ i E.^. InstanceId, E.asc $ ra E.^. RemoteActorId]
return
( i E.^. InstanceId
, i E.^. InstanceHost
, ra E.^. RemoteActorId
, ro E.^. RemoteObjectIdent
, ra E.^. RemoteActorInbox
, ra E.^. RemoteActorErrorSince
)
return
( map (followPerson . entityVal) local
, groupRemotes $
map (\ (E.Value iid, E.Value h, E.Value raid, E.Value luActor, E.Value luInbox, E.Value msince) ->
(iid, h, raid, luActor, luInbox, msince)
)
remote
)
where
groupRemotes :: [(InstanceId, Host, RemoteActorId, LocalURI, LocalURI, Maybe UTCTime)] -> [((InstanceId, Host), NonEmpty RemoteRecipient)]
groupRemotes = groupWithExtractBy ((==) `on` fst) fst snd . map toTuples
where
toTuples (iid, h, raid, luA, luI, ms) = ((iid, h), RemoteRecipient raid luA luI ms)
unionRemotes
:: [((InstanceId, Host), NonEmpty RemoteRecipient)]
-> [((InstanceId, Host), NonEmpty RemoteRecipient)]
-> [((InstanceId, Host), NonEmpty RemoteRecipient)]
unionRemotes = unionGroupsOrdWith fst remoteRecipientActor
insertMany' mk xs = zip' xs <$> insertMany (NE.toList $ mk <$> xs)
where
zip' x y =
case nonEmpty y of
Just y' | length x == length y' -> NE.zip x y'
_ -> error "insertMany' returned different length!"
isInstanceErrorHttp (InvalidUrlException _ _) = False
isInstanceErrorHttp (HttpExceptionRequest _ hec) =
case hec of
ResponseTimeout -> True
ConnectionTimeout -> True
InternalException se ->
case fromException se of
Just (HandshakeFailed _) -> True
_ -> False
_ -> False
isInstanceErrorP (APPostErrorSig _) = False
isInstanceErrorP (APPostErrorHTTP he) = isInstanceErrorHttp he
isInstanceErrorG Nothing = False
isInstanceErrorG (Just e) =
case e of
APGetErrorHTTP he -> isInstanceErrorHttp he
APGetErrorJSON _ -> False
APGetErrorContentType _ -> False
deliverHttp
:: (MonadSite m, SiteEnv m ~ App)
=> Doc Activity URIMode
-> Maybe LocalURI
-> Host
-> LocalURI
-> m (Either APPostError (Response ()))
deliverHttp doc mfwd h luInbox =
deliverActivity (ObjURI h luInbox) (ObjURI h <$> mfwd) doc
deliverHttpBL
:: (MonadSite m, SiteEnv m ~ App)
=> BL.ByteString
-> Maybe LocalURI
-> Host
-> LocalURI
-> m (Either APPostError (Response ()))
deliverHttpBL body mfwd h luInbox =
deliverActivityBL' (ObjURI h luInbox) (ObjURI h <$> mfwd) body
deliverRemoteDB
:: BL.ByteString
-> RemoteActivityId
-> ProjectId
-> ByteString
-> [((InstanceId, Host), NonEmpty RemoteRecipient)]
-> AppDB
[((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId))]
deliverRemoteDB body ractid jid sig recips = do
let body' = BL.toStrict body
deliv raid msince = Forwarding raid ractid body' jid sig $ isNothing msince
fetchedDeliv <- for recips $ \ (i, rs) ->
(i,) <$> insertMany' (\ (RemoteRecipient raid _ _ msince) -> deliv raid msince) rs
return $ takeNoError4 fetchedDeliv
where
takeNoError noError = mapMaybe $ \ (i, rs) -> (i,) <$> nonEmpty (mapMaybe noError $ NE.toList rs)
takeNoError4 = takeNoError noError
where
noError (RemoteRecipient ak luA luI Nothing , dlk) = Just (ak, luA, luI, dlk)
noError (RemoteRecipient _ _ _ (Just _), _ ) = Nothing
deliverRemoteHTTP
:: UTCTime
-> ShrIdent
-> PrjIdent
-> BL.ByteString
-> ByteString
-> [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId))]
-> Handler ()
deliverRemoteHTTP now shrRecip prjRecip body sig fetched = do
let deliver h inbox =
let sender = ProjectR shrRecip prjRecip
in forwardActivity (ObjURI h inbox) sig sender body
traverse_ (fork . deliverFetched deliver now) fetched
where
fork = forkHandler $ \ e -> logError $ "Project inbox handler: delivery failed! " <> T.pack (displayException e)
deliverFetched deliver now ((_, h), recips@(r :| rs)) = do
let (raid, _luActor, luInbox, fwid) = r
e <- deliver h luInbox
let e' = case e of
Left err ->
if isInstanceErrorP err
then Nothing
else Just False
Right _resp -> Just True
case e' of
Nothing -> runDB $ do
let recips' = NE.toList recips
updateWhere [RemoteActorId <-. map fst4 recips', RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
updateWhere [ForwardingId <-. map fourth4 recips'] [ForwardingRunning =. False]
Just success -> do
runDB $
if success
then delete fwid
else do
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
update fwid [ForwardingRunning =. False]
for_ rs $ \ (raid, _luActor, luInbox, fwid) ->
fork $ do
e <- deliver h luInbox
runDB $
case e of
Left _err -> do
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
update fwid [ForwardingRunning =. False]
Right _resp -> delete fwid
checkForward shrRecip prjRecip = join <$> do
let hSig = hForwardingSignature
msig <- maybeHeader hSig
for msig $ \ sig -> do
_proof <- withExceptT (T.pack . displayException) $ ExceptT $
let requires = [hDigest, hActivityPubForwarder]
in prepareToVerifyHttpSigWith hSig False requires [] Nothing
forwarder <- requireHeader hActivityPubForwarder
renderUrl <- getUrlRender
let project = renderUrl $ ProjectR shrRecip prjRecip
return $
if forwarder == encodeUtf8 project
then Just sig
else Nothing
where
maybeHeader n = do
let n' = decodeUtf8 $ CI.original n
hs <- lookupHeaders n
case hs of
[] -> return Nothing
[h] -> return $ Just h
_ -> throwE $ n' <> " multiple headers found"
requireHeader n = do
let n' = decodeUtf8 $ CI.original n
mh <- maybeHeader n
case mh of
Nothing -> throwE $ n' <> " header not found"
Just h -> return h
parseTarget u = do
let ObjURI h lu = u
(shr, prj) <- parseProject lu
return (h, shr, prj)
where
parseProject lu = do
route <- case decodeRouteLocal lu of
Nothing -> throwE "Expected project route, got invalid route"
Just r -> return r
case route of
ProjectR shr prj -> return (shr, prj)
_ -> throwE "Expected project route, got non-project route"
{-
checkDep hProject shrProject prjProject u = do
let (h, lu) = f2l u
unless (h == hProject) $
throwE "Dep belongs to different host"
(shrTicket, prjTicket, num) <- parseTicket lu
unless (shrTicket == shrProject) $
throwE "Dep belongs to different sharer under same host"
unless (prjTicket == prjProject) $
throwE "Dep belongs to different project under same sharer"
return num
where
parseTicket lu = do
route <- case decodeRouteLocal lu of
Nothing -> throwE "Expected ticket route, got invalid route"
Just r -> return r
case route of
TicketR shr prj num -> return (shr, prj, num)
_ -> throwE "Expected ticket route, got non-ticket route"
-}
getProjectAndDeps shr prj {-deps-} = do
msid <- lift $ getKeyBy $ UniqueSharer shr
sid <- fromMaybeE msid "Offer target: no such local sharer"
mej <- lift $ getBy $ UniqueProject prj sid
Entity jid j <- fromMaybeE mej "Offer target: no such local project"
{-
tids <- for deps $ \ dep -> do
mtid <- lift $ getKeyBy $ UniqueTicket jid dep
fromMaybeE mtid "Local dep: No such ticket number in DB"
-}
return (sid, jid, projectInbox j, projectFollowers j{-, tids-})
data Recip
= RecipRA (Entity RemoteActor)
| RecipURA (Entity UnfetchedRemoteActor)
| RecipRC (Entity RemoteCollection)
deliverRemoteDB'
:: Host
-> OutboxItemId
-> [(Host, NonEmpty LocalURI)]
-> [((InstanceId, Host), NonEmpty RemoteRecipient)]
-> AppDB
( [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, DeliveryId))]
, [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))]
, [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))]
)
deliverRemoteDB' hContext obid recips known = do
recips' <- for recips $ \ (h, lus) -> do
let lus' = NE.nub lus
(iid, inew) <- idAndNew <$> insertBy' (Instance h)
if inew
then return ((iid, h), (Nothing, Nothing, Just lus'))
else do
es <- for lus' $ \ lu -> do
ma <- runMaybeT $ do
Entity roid ro <- MaybeT $ getBy $ UniqueRemoteObject iid lu
recip <- RecipRA <$> MaybeT (getBy $ UniqueRemoteActor roid)
<|> RecipURA <$> MaybeT (getBy $ UniqueUnfetchedRemoteActor roid)
<|> RecipRC <$> MaybeT (getBy $ UniqueRemoteCollection roid)
return (ro, recip)
return $
case ma of
Nothing -> Just $ Left lu
Just (ro, r) ->
case r of
RecipRA (Entity raid ra) -> Just $ Right $ Left $ RemoteRecipient raid (remoteObjectIdent ro) (remoteActorInbox ra) (remoteActorErrorSince ra)
RecipURA (Entity uraid ura) -> Just $ Right $ Right (uraid, remoteObjectIdent ro, unfetchedRemoteActorSince ura)
RecipRC _ -> Nothing
let (unknown, newKnown) = partitionEithers $ catMaybes $ NE.toList es
(fetched, unfetched) = partitionEithers newKnown
return ((iid, h), (nonEmpty fetched, nonEmpty unfetched, nonEmpty unknown))
let moreKnown = mapMaybe (\ (i, (f, _, _)) -> (i,) <$> f) recips'
unfetched = mapMaybe (\ (i, (_, uf, _)) -> (i,) <$> uf) recips'
stillUnknown = mapMaybe (\ (i, (_, _, uk)) -> (i,) <$> uk) recips'
allFetched = unionRemotes known moreKnown
fetchedDeliv <- for allFetched $ \ (i, rs) ->
let fwd = snd i == hContext
in (i,) <$> insertMany' (\ (RemoteRecipient raid _ _ msince) -> Delivery raid obid fwd $ isNothing msince) rs
unfetchedDeliv <- for unfetched $ \ (i, rs) ->
let fwd = snd i == hContext
in (i,) <$> insertMany' (\ (uraid, _, msince) -> UnlinkedDelivery uraid obid fwd $ isNothing msince) rs
unknownDeliv <- for stillUnknown $ \ (i, lus) -> do
-- TODO maybe for URA insertion we should do insertUnique?
ros <- insertMany' (\ lu -> RemoteObject (fst i) lu) lus
rs <- insertMany' (\ (_lu, roid) -> UnfetchedRemoteActor roid Nothing) ros
let fwd = snd i == hContext
(i,) <$> insertMany' (\ (_, uraid) -> UnlinkedDelivery uraid obid fwd True) rs
return
( takeNoError4 fetchedDeliv
, takeNoError3 unfetchedDeliv
, map
(second $ NE.map $ \ (((lu, _roid), ak), dlk) -> (ak, lu, dlk))
unknownDeliv
)
where
takeNoError noError = mapMaybe $ \ (i, rs) -> (i,) <$> nonEmpty (mapMaybe noError $ NE.toList rs)
takeNoError3 = takeNoError noError
where
noError ((ak, lu, Nothing), dlk) = Just (ak, lu, dlk)
noError ((_ , _ , Just _ ), _ ) = Nothing
takeNoError4 = takeNoError noError
where
noError (RemoteRecipient ak luA luI Nothing , dlk) = Just (ak, luA, luI, dlk)
noError (RemoteRecipient _ _ _ (Just _), _ ) = Nothing
deliverRemoteHttp
:: Host
-> OutboxItemId
-> Doc Activity URIMode
-> ( [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, DeliveryId))]
, [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))]
, [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))]
)
-> Worker ()
deliverRemoteHttp hContext obid doc (fetched, unfetched, unknown) = do
logDebug' "Starting"
let deliver fwd h inbox = do
let fwd' = if h == hContext then Just fwd else Nothing
(isJust fwd',) <$> deliverHttp doc fwd' h inbox
now <- liftIO getCurrentTime
logDebug' $
"Launching fetched " <> showHosts fetched
traverse_ (fork . deliverFetched deliver now) fetched
logDebug' $
"Launching unfetched " <> showHosts unfetched
traverse_ (fork . deliverUnfetched deliver now) unfetched
logDebug' $
"Launching unknown " <> showHosts unknown
traverse_ (fork . deliverUnfetched deliver now) unknown
logDebug' "Done (async delivery may still be running)"
where
showHosts = T.pack . show . map (renderAuthority . snd . fst)
logDebug' t = logDebug $ prefix <> t
where
prefix =
T.concat
[ "Outbox POST handler: deliverRemoteHttp obid#"
, T.pack $ show $ fromSqlKey obid
, ": "
]
fork = forkWorker "Outbox POST handler: HTTP delivery"
deliverFetched deliver now ((_, h), recips@(r :| rs)) = do
logDebug'' "Starting"
let (raid, luActor, luInbox, dlid) = r
(_, e) <- deliver luActor h luInbox
e' <- case e of
Left err -> do
logError $ T.concat
[ "Outbox DL delivery #", T.pack $ show dlid
, " error for <", renderObjURI $ ObjURI h luActor
, ">: ", T.pack $ displayException err
]
return $
if isInstanceErrorP err
then Nothing
else Just False
Right _resp -> return $ Just True
case e' of
Nothing -> runSiteDB $ do
let recips' = NE.toList recips
updateWhere [RemoteActorId <-. map fst4 recips', RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
updateWhere [DeliveryId <-. map fourth4 recips'] [DeliveryRunning =. False]
Just success -> do
runSiteDB $
if success
then delete dlid
else do
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
update dlid [DeliveryRunning =. False]
for_ rs $ \ (raid, luActor, luInbox, dlid) ->
fork $ do
(_, e) <- deliver luActor h luInbox
runSiteDB $
case e of
Left err -> do
logError $ T.concat
[ "Outbox DL delivery #", T.pack $ show dlid
, " error for <", renderObjURI $ ObjURI h luActor
, ">: ", T.pack $ displayException err
]
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
update dlid [DeliveryRunning =. False]
Right _resp -> delete dlid
where
logDebug'' t = logDebug' $ T.concat ["deliverFetched ", renderAuthority h, t]
deliverUnfetched deliver now ((iid, h), recips@(r :| rs)) = do
logDebug'' "Starting"
let (uraid, luActor, udlid) = r
e <- fetchRemoteActor iid h luActor
let e' = case e of
Left err -> Just Nothing
Right (Left err) ->
if isInstanceErrorG err
then Nothing
else Just Nothing
Right (Right mera) -> Just $ Just mera
case e' of
Nothing -> runSiteDB $ do
let recips' = NE.toList recips
updateWhere [UnfetchedRemoteActorId <-. map fst3 recips', UnfetchedRemoteActorSince ==. Nothing] [UnfetchedRemoteActorSince =. Just now]
updateWhere [UnlinkedDeliveryId <-. map thd3 recips'] [UnlinkedDeliveryRunning =. False]
Just mmera -> do
for_ rs $ \ (uraid, luActor, udlid) ->
fork $ do
e <- fetchRemoteActor iid h luActor
case e of
Right (Right mera) ->
case mera of
Nothing -> runSiteDB $ delete udlid
Just (Entity raid ra) -> do
(fwd, e') <- deliver luActor h $ remoteActorInbox ra
runSiteDB $
case e' of
Left _ -> do
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
delete udlid
insert_ $ Delivery raid obid fwd False
Right _ -> delete udlid
_ -> runSiteDB $ do
updateWhere [UnfetchedRemoteActorId ==. uraid, UnfetchedRemoteActorSince ==. Nothing] [UnfetchedRemoteActorSince =. Just now]
update udlid [UnlinkedDeliveryRunning =. False]
case mmera of
Nothing -> runSiteDB $ do
updateWhere [UnfetchedRemoteActorId ==. uraid, UnfetchedRemoteActorSince ==. Nothing] [UnfetchedRemoteActorSince =. Just now]
update udlid [UnlinkedDeliveryRunning =. False]
Just mera ->
case mera of
Nothing -> runSiteDB $ delete udlid
Just (Entity raid ra) -> do
(fwd, e'') <- deliver luActor h $ remoteActorInbox ra
runSiteDB $
case e'' of
Left _ -> do
updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now]
delete udlid
insert_ $ Delivery raid obid fwd False
Right _ -> delete udlid
where
logDebug'' t = logDebug' $ T.concat ["deliverUnfetched ", renderAuthority h, t]
serveCommit
:: ShrIdent
-> RpIdent
-> Text
-> Patch
-> [Text]
-> Handler TypedContent
serveCommit shr rp ref patch parents = do
(msharerWritten, msharerCommitted) <- runDB $ (,)
<$> getSharer (patchWritten patch)
<*> maybe (pure Nothing) getSharer (patchCommitted patch)
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let (author, written) = patchWritten patch
mcommitter = patchCommitted patch
patchAP = AP.Commit
{ commitId =
encodeRouteLocal $ RepoPatchR shr rp ref
, commitRepository = encodeRouteLocal $ RepoR shr rp
, commitAuthor =
makeAuthor encodeRouteHome msharerWritten author
, commitCommitter =
makeAuthor encodeRouteHome msharerCommitted . fst <$>
mcommitter
, commitTitle = patchTitle patch
, commitHash = Hash $ encodeUtf8 ref
, commitDescription =
let desc = patchDescription patch
in if T.null desc
then Nothing
else Just desc
, commitWritten = written
, commitCommitted = snd <$> patchCommitted patch
}
provideHtmlAndAP patchAP $
let number = zip ([1..] :: [Int])
in $(widgetFile "repo/patch")
where
getSharer (author, _time) = do
mp <- getBy $ UniquePersonEmail $ authorEmail author
for mp $ \ (Entity _ person) -> getJust $ personIdent person
makeAuthor _ Nothing author = Left AP.Author
{ AP.authorName = authorName author
, AP.authorEmail = authorEmail author
}
makeAuthor encodeRouteHome (Just sharer) _ =
Right $ encodeRouteHome $ SharerR $ sharerIdent sharer
-- | Given a list of local recipients, which may include actors and
-- collections,
--
-- * Insert activity to inboxes of actors
-- * If the author's follower collection is listed, insert activity to the
-- local members and return the remote members
-- * Ignore other collections
deliverLocal
:: ShrIdent
-> InboxId
-> FollowerSetId
-> OutboxItemId
-> LocalRecipientSet
-> AppDB
[ ( (InstanceId, Host)
, NonEmpty RemoteRecipient
)
]
deliverLocal shrAuthor ibidAuthor _fsidAuthor obiid = deliverLocal' True (LocalActorSharer shrAuthor) ibidAuthor obiid . localRecipSieve sieve True
where
sieve = [(shrAuthor, LocalSharerRelatedSet (LocalSharerDirectSet False True) [] [] [])]
data RemoteRecipient = RemoteRecipient
{ remoteRecipientActor :: RemoteActorId
, remoteRecipientId :: LocalURI
, remoteRecipientInbox :: LocalURI
, remoteRecipientErrorSince :: Maybe UTCTime
}
-- | Given a list of local recipients, which may include actors and
-- collections,
--
-- * Insert activity to inboxes of actors
-- * If collections are listed, insert activity to the local members and return
-- the remote members
deliverLocal'
:: Bool -- ^ Whether to deliver to collection only if owner actor is addressed
-> LocalActor
-> InboxId
-> OutboxItemId
-> LocalRecipientSet
-> AppDB [((InstanceId, Host), NonEmpty RemoteRecipient)]
deliverLocal' requireOwner author ibidAuthor obiid recips = do
ibidsSharer <- L.delete ibidAuthor <$> getSharerInboxes recips
ibidsOther <- concat <$> traverse getOtherInboxes recips
(ibidsFollowers, remotesFollowers) <- do
fsidsSharer <- getSharerFollowerSets recips
fsidsOther <- concat <$> traverse getOtherFollowerSets recips
let fsids = fsidsSharer ++ fsidsOther
(,) <$> getLocalFollowers fsids <*> getRemoteFollowers fsids
ibidsTeams <- foldl' LO.union [] <$> traverse getTeams recips
let ibids = L.delete ibidAuthor (ibidsFollowers `LO.union` ibidsTeams `LO.union` ibidsSharer) ++ ibidsOther
ibiids <- insertMany $ replicate (length ibids) $ InboxItem True
insertMany_ $
map (\ (ibid, ibiid) -> InboxItemLocal ibid obiid ibiid)
(zip ibids ibiids)
return remotesFollowers
where
getSharerInboxes :: LocalRecipientSet -> AppDB [InboxId]
getSharerInboxes sharers = do
let shrs =
[shr | (shr, s) <- sharers
, localRecipSharer $ localRecipSharerDirect s
]
sids <- selectKeysList [SharerIdent <-. shrs] []
map (personInbox . entityVal) <$> selectList [PersonIdent <-. sids] [Asc PersonInbox]
getOtherInboxes :: (ShrIdent, LocalSharerRelatedSet) -> AppDB [InboxId]
getOtherInboxes (shr, LocalSharerRelatedSet _ _ projects repos) = do
msid <- getKeyBy $ UniqueSharer shr
case msid of
Nothing -> return []
Just sid ->
(++)
<$> getProjectInboxes sid projects
<*> getRepoInboxes sid repos
where
getProjectInboxes sid projects =
let prjs =
[prj | (prj, j) <- projects
, localRecipProject $ localRecipProjectDirect j
]
in map (projectInbox . entityVal) <$>
selectList [ProjectSharer ==. sid, ProjectIdent <-. prjs] []
getRepoInboxes sid repos =
let rps =
[rp | (rp, r) <- repos
, localRecipRepo $ localRecipRepoDirect r
]
in map (repoInbox . entityVal) <$>
selectList [RepoSharer ==. sid, RepoIdent <-. rps] []
getSharerFollowerSets :: LocalRecipientSet -> AppDB [FollowerSetId]
getSharerFollowerSets sharers = do
let shrs =
[shr | (shr, s) <- sharers
, let d = localRecipSharerDirect s
in localRecipSharerFollowers d &&
(localRecipSharer d || not requireOwner || LocalActorSharer shr == author)
]
sids <- selectKeysList [SharerIdent <-. shrs] []
map (personFollowers . entityVal) <$> selectList [PersonIdent <-. sids] []
getOtherFollowerSets :: (ShrIdent, LocalSharerRelatedSet) -> AppDB [FollowerSetId]
getOtherFollowerSets (shr, LocalSharerRelatedSet _ tickets projects repos) = do
msid <- getKeyBy $ UniqueSharer shr
case msid of
Nothing -> return []
Just sid -> do
mpid <- getKeyBy $ UniquePersonIdent sid
(\ t j r -> map E.unValue t ++ j ++ r)
<$> case mpid of
Nothing -> pure []
Just pid -> getSharerTicketFollowerSets pid tickets
<*> getProjectFollowerSets sid projects
<*> getRepoFollowerSets sid repos
where
getSharerTicketFollowerSets pid tickets = do
let talkhids =
[talkhid | (talkhid, t) <- tickets
, localRecipTicketFollowers t
]
talids <- catMaybes <$> traverse decodeKeyHashid talkhids
E.select $ E.from $ \ (tal `E.InnerJoin` lt `E.LeftOuterJoin` tup) -> do
E.on $ E.just (tal E.^. TicketAuthorLocalId) E.==. tup E.?. TicketUnderProjectAuthor
E.on $ tal E.^. TicketAuthorLocalTicket E.==. lt E.^. LocalTicketId
E.where_ $
tal E.^. TicketAuthorLocalAuthor E.==. E.val pid E.&&.
E.isNothing (tup E.?. TicketUnderProjectId)
return $ lt E.^. LocalTicketFollowers
getProjectFollowerSets sid projects = do
let prjsJ =
[prj | (prj, j) <- projects
, let d = localRecipProjectDirect j
in localRecipProjectFollowers d &&
(localRecipProject d || not requireOwner || LocalActorProject shr prj == author)
]
fsidsJ <-
map (projectFollowers . entityVal) <$>
selectList [ProjectSharer ==. sid, ProjectIdent <-. prjsJ] []
let prjsT =
if requireOwner
then
[ (prj, localRecipProjectTicketRelated j)
| (prj, j) <- projects
, localRecipProject (localRecipProjectDirect j) || LocalActorProject shr prj == author
]
else
map (second localRecipProjectTicketRelated) projects
fsidssT <- for prjsT $ \ (prj, tickets) -> do
mjid <- getKeyBy $ UniqueProject prj sid
case mjid of
Nothing -> return []
Just jid -> getTicketFollowerSets jid tickets
return $ fsidsJ ++ map E.unValue (concat fsidssT)
where
getTicketFollowerSets jid tickets = do
let ltkhids =
[ltkhid | (ltkhid, t) <- tickets
, localRecipTicketFollowers t
]
ltids <- catMaybes <$> traverse decodeKeyHashid ltkhids
E.select $ E.from $ \ (lt `E.InnerJoin` t `E.InnerJoin` tpl `E.LeftOuterJoin` tup `E.LeftOuterJoin` tar) -> do
E.on $ E.just (tpl E.^. TicketProjectLocalId) E.==. tar E.?. TicketAuthorRemoteTicket
E.on $ E.just (tpl E.^. TicketProjectLocalId) E.==. tup E.?. TicketUnderProjectProject
E.on $ t E.^. TicketId E.==. tpl E.^. TicketProjectLocalTicket
E.on $ lt E.^. LocalTicketTicket E.==. t E.^. TicketId
E.where_ $
tpl E.^. TicketProjectLocalProject E.==. E.val jid E.&&.
E.not_
( E.isNothing (tup E.?. TicketUnderProjectId) E.&&.
E.isNothing (tar E.?. TicketAuthorRemoteId)
)
return $ lt E.^. LocalTicketFollowers
getRepoFollowerSets sid repos =
let rps =
[rp | (rp, r) <- repos
, let d = localRecipRepoDirect r
in localRecipRepoFollowers d &&
(localRecipRepo d || not requireOwner || LocalActorRepo shr rp == author)
]
in map (repoFollowers . entityVal) <$>
selectList [RepoSharer ==. sid, RepoIdent <-. rps] []
getLocalFollowers :: [FollowerSetId] -> AppDB [InboxId]
getLocalFollowers fsids = do
pids <-
map (followPerson . entityVal) <$>
selectList [FollowTarget <-. fsids] []
map (personInbox . entityVal) <$>
selectList [PersonId <-. pids] [Asc PersonInbox]
getRemoteFollowers :: [FollowerSetId] -> AppDB [((InstanceId, Host), NonEmpty RemoteRecipient)]
getRemoteFollowers fsids =
fmap groupRemotes $
E.select $ E.from $ \ (rf `E.InnerJoin` ra `E.InnerJoin` ro `E.InnerJoin` i) -> do
E.on $ ro E.^. RemoteObjectInstance E.==. i E.^. InstanceId
E.on $ ra E.^. RemoteActorIdent E.==. ro E.^. RemoteObjectId
E.on $ rf E.^. RemoteFollowActor E.==. ra E.^. RemoteActorId
E.where_ $ rf E.^. RemoteFollowTarget `E.in_` E.valList fsids
E.orderBy [E.asc $ i E.^. InstanceId, E.asc $ ra E.^. RemoteActorId]
return
( i E.^. InstanceId
, i E.^. InstanceHost
, ra E.^. RemoteActorId
, ro E.^. RemoteObjectIdent
, ra E.^. RemoteActorInbox
, ra E.^. RemoteActorErrorSince
)
where
groupRemotes = groupWithExtractBy ((==) `on` fst) fst snd . map toTuples
where
toTuples (E.Value iid, E.Value h, E.Value raid, E.Value luA, E.Value luI, E.Value ms) = ((iid, h), RemoteRecipient raid luA luI ms)
getTeams :: (ShrIdent, LocalSharerRelatedSet) -> AppDB [InboxId]
getTeams (shr, LocalSharerRelatedSet _ tickets projects repos) = do
msid <- getKeyBy $ UniqueSharer shr
case msid of
Nothing -> return []
Just sid -> do
mpid <- getKeyBy $ UniquePersonIdent sid
(\ t j r -> t `LO.union` j `LO.union` r)
<$> case mpid of
Nothing -> pure []
Just pid -> getSharerTicketTeams pid tickets
<*> getProjectTeams sid projects
<*> getRepoTeams sid repos
where
getSharerTicketTeams _pid _tickets = pure []
getProjectTeams sid projects = do
let prjs =
[prj | (prj, LocalProjectRelatedSet d ts) <- projects
, (localRecipProject d || not requireOwner || LocalActorProject shr prj == author) &&
(localRecipProjectTeam d || any (localRecipTicketTeam . snd) ts)
]
jids <- selectKeysList [ProjectSharer ==. sid, ProjectIdent <-. prjs] []
pids <- map (projectCollabPerson . entityVal) <$> selectList [ProjectCollabProject <-. jids] []
map (personInbox . entityVal) <$> selectList [PersonId <-. pids] [Asc PersonInbox]
getRepoTeams sid repos = do
let rps =
[rp | (rp, r) <- repos
, let d = localRecipRepoDirect r
in localRecipRepoTeam d &&
(localRecipRepo d || not requireOwner || LocalActorRepo shr rp == author)
]
rids <- selectKeysList [RepoSharer ==. sid, RepoIdent <-. rps] []
pids <- map (repoCollabPerson . entityVal) <$> selectList [RepoCollabRepo <-. rids] []
map (personInbox . entityVal) <$> selectList [PersonId <-. pids] [Asc PersonInbox]