{- 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 ( NoteContext (..) , parseContext , parseParent , getLocalParentMessageId , getPersonOrGroupId , getTicketTeam , getProjectTeam , getRepoTeam , getFollowers , unionRemotes , insertMany' , isInstanceErrorP , isInstanceErrorG , deliverHttp , deliverHttpBL , deliverRemoteDB_J , deliverRemoteDB_S , deliverRemoteDB_R , deliverRemoteHTTP_J , deliverRemoteHTTP_S , deliverRemoteHTTP_R , checkForward , parseTarget --, checkDep , getProjectAndDeps , deliverRemoteDB' , deliverRemoteDB'' , deliverRemoteHttp , deliverRemoteHttp' , serveCommit , deliverLocal , RemoteRecipient (..) , deliverLocal' , insertRemoteActivityToLocalInboxes , provideEmptyCollection , insertEmptyOutboxItem ) 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.Bitraversable 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.Patch.Local hiding (Patch) import Data.Tuple.Local import Database.Persist.Local import qualified Data.Patch.Local as P import Vervis.ActivityPub.Recipient import Vervis.FedURI import Vervis.Foundation import Vervis.Model import Vervis.Model.Ident import Vervis.RemoteActorStore import Vervis.Settings import Vervis.Time import Vervis.Widget.Repo import Vervis.Widget.Sharer data NoteContext = NoteContextSharerTicket ShrIdent TicketAuthorLocalId Bool | NoteContextProjectTicket ShrIdent PrjIdent LocalTicketId | NoteContextRepoPatch ShrIdent RpIdent LocalTicketId deriving Eq parseContext :: (MonadSite m, SiteEnv m ~ App) => FedURI -> ExceptT Text m (Either NoteContext 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 SharerTicketR shr talkhid -> flip (NoteContextSharerTicket shr) False <$> decodeKeyHashidE talkhid "Note context invalid talkhid" SharerPatchR shr talkhid -> flip (NoteContextSharerTicket shr) True <$> decodeKeyHashidE talkhid "Note context invalid talkhid" ProjectTicketR shr prj ltkhid -> NoteContextProjectTicket shr prj <$> decodeKeyHashidE ltkhid "Note context invalid ltkhid" RepoPatchR shr rp ltkhid -> NoteContextRepoPatch shr rp <$> decodeKeyHashidE ltkhid "Note context invalid ltkhid" _ -> throwE "Local context isn't a ticket/patch 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 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_ :: (MonadIO m, PersistRecordBackend fwder SqlBackend) => (ForwardingId -> Key sender -> fwder) -> BL.ByteString -> RemoteActivityId -> Key sender -> ByteString -> [((InstanceId, Host), NonEmpty RemoteRecipient)] -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, Key fwder))] deliverRemoteDB_ makeFwder body ractid senderKey sig recips = do let body' = BL.toStrict body makeFwd (RemoteRecipient raid _ _ msince) = Forwarding raid ractid body' sig (isNothing msince) fetchedDeliv <- for recips $ bitraverse pure $ \ rs -> do fwds <- insertMany' makeFwd rs insertMany' (flip makeFwder senderKey . snd) fwds return $ takeNoError5 fetchedDeliv where takeNoError noError = mapMaybe $ \ (i, rs) -> (i,) <$> nonEmpty (mapMaybe noError $ NE.toList rs) takeNoError5 = takeNoError noError where noError ((RemoteRecipient ak luA luI Nothing , fwid), fwrid) = Just (ak, luA, luI, fwid, fwrid) noError ((RemoteRecipient _ _ _ (Just _), _ ), _ ) = Nothing deliverRemoteDB_J :: MonadIO m => BL.ByteString -> RemoteActivityId -> ProjectId -> ByteString -> [((InstanceId, Host), NonEmpty RemoteRecipient)] -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderProjectId))] deliverRemoteDB_J = deliverRemoteDB_ ForwarderProject deliverRemoteDB_S :: MonadIO m => BL.ByteString -> RemoteActivityId -> SharerId -> ByteString -> [((InstanceId, Host), NonEmpty RemoteRecipient)] -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderSharerId))] deliverRemoteDB_S = deliverRemoteDB_ ForwarderSharer deliverRemoteDB_R :: MonadIO m => BL.ByteString -> RemoteActivityId -> RepoId -> ByteString -> [((InstanceId, Host), NonEmpty RemoteRecipient)] -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderRepoId))] deliverRemoteDB_R = deliverRemoteDB_ ForwarderRepo deliverRemoteHTTP' :: (MonadSite m, SiteEnv m ~ App, PersistRecordBackend fwder SqlBackend) => UTCTime -> LocalActor -> BL.ByteString -> ByteString -> [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, Key fwder))] -> m () deliverRemoteHTTP' now sender body sig fetched = do let deliver h inbox = forwardActivity (ObjURI h inbox) sig (renderLocalActor sender) body traverse_ (fork . deliverFetched deliver now) fetched where fork = forkWorker "Inbox forwarding to remote members of local collections: delivery failed" deliverFetched deliver now ((_, h), recips@(r :| rs)) = do let (raid, _luActor, luInbox, fwid, forwarderKey) = 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 -> runSiteDB $ do let recips' = NE.toList recips updateWhere [RemoteActorId <-. map fst5 recips', RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now] updateWhere [ForwardingId <-. map fourth5 recips'] [ForwardingRunning =. False] Just success -> do runSiteDB $ if success then do delete forwarderKey delete fwid else do updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now] update fwid [ForwardingRunning =. False] for_ rs $ \ (raid, _luActor, luInbox, fwid, forwarderKey) -> fork $ do e <- deliver h luInbox runSiteDB $ case e of Left _err -> do updateWhere [RemoteActorId ==. raid, RemoteActorErrorSince ==. Nothing] [RemoteActorErrorSince =. Just now] update fwid [ForwardingRunning =. False] Right _resp -> do delete forwarderKey delete fwid deliverRemoteHTTP_J :: (MonadSite m, SiteEnv m ~ App) => UTCTime -> ShrIdent -> PrjIdent -> BL.ByteString -> ByteString -> [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderProjectId))] -> m () deliverRemoteHTTP_J now shr prj = deliverRemoteHTTP' now $ LocalActorProject shr prj deliverRemoteHTTP_S :: (MonadSite m, SiteEnv m ~ App) => UTCTime -> ShrIdent -> BL.ByteString -> ByteString -> [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderSharerId))] -> m () deliverRemoteHTTP_S now shr = deliverRemoteHTTP' now $ LocalActorSharer shr deliverRemoteHTTP_R :: (MonadSite m, SiteEnv m ~ App) => UTCTime -> ShrIdent -> RpIdent -> BL.ByteString -> ByteString -> [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, ForwardingId, ForwarderRepoId))] -> m () deliverRemoteHTTP_R now shr rp = deliverRemoteHTTP' now $ LocalActorRepo shr rp checkForward recip = 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 return $ if forwarder == encodeUtf8 (renderUrl $ renderLocalActor recip) 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 = deliverRemoteDB'' [hContext] deliverRemoteDB'' :: MonadIO m => [Host] -> OutboxItemId -> [(Host, NonEmpty LocalURI)] -> [((InstanceId, Host), NonEmpty RemoteRecipient)] -> ReaderT SqlBackend m ( [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, DeliveryId))] , [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))] , [((InstanceId, Host), NonEmpty (UnfetchedRemoteActorId, LocalURI, UnlinkedDeliveryId))] ) deliverRemoteDB'' hContexts 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 `elem` hContexts in (i,) <$> insertMany' (\ (RemoteRecipient raid _ _ msince) -> Delivery raid obid fwd $ isNothing msince) rs unfetchedDeliv <- for unfetched $ \ (i, rs) -> let fwd = snd i `elem` hContexts 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 `elem` hContexts (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 = deliverRemoteHttp' [hContext] 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' hContexts obid doc (fetched, unfetched, unknown) = do logDebug' "Starting" let deliver fwd h inbox = do let fwd' = if h `elem` hContexts 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 -> P.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 $ RepoCommitR 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 insertActivityToLocalInboxes :: ( MonadSite m , YesodHashids (SiteEnv m) , PersistRecordBackend record SqlBackend ) => (InboxId -> InboxItemId -> record) -- ^ Database record to insert as an new inbox item to each inbox -> Bool -- ^ Whether to deliver to collection only if owner actor is addressed -> Maybe LocalActor -- ^ An actor whose collections are excluded from requiring an owner, i.e. -- even if owner is required, this actor's collections will be delivered -- to, even if this actor isn't addressed. This is meant to be the -- activity's author. -> Maybe InboxId -- ^ A user person's inbox to exclude from delivery, even if this person is -- listed in the recipient set. This is meant to be the activity's -- author. -> LocalRecipientSet -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty RemoteRecipient)] insertActivityToLocalInboxes makeInboxItem requireOwner mauthor mibidAuthor recips = do ibidsSharer <- deleteAuthor <$> 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 = deleteAuthor (ibidsFollowers `LO.union` ibidsTeams `LO.union` ibidsSharer) ++ ibidsOther ibiids <- insertMany $ replicate (length ibids) $ InboxItem True insertMany_ $ zipWith makeInboxItem ibids ibiids return remotesFollowers where isAuthor :: LocalActor -> Bool isAuthor = case mauthor of Nothing -> const False Just author -> (== author) deleteAuthor :: [InboxId] -> [InboxId] deleteAuthor = case mibidAuthor of Nothing -> id Just ibidAuthor -> L.delete ibidAuthor getSharerInboxes :: MonadIO m => LocalRecipientSet -> ReaderT SqlBackend m [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 :: MonadIO m => (ShrIdent, LocalSharerRelatedSet) -> ReaderT SqlBackend m [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 :: MonadIO m => LocalRecipientSet -> ReaderT SqlBackend m [FollowerSetId] getSharerFollowerSets sharers = do let shrs = [shr | (shr, s) <- sharers , let d = localRecipSharerDirect s in localRecipSharerFollowers d && (localRecipSharer d || not requireOwner || isAuthor (LocalActorSharer shr)) ] sids <- selectKeysList [SharerIdent <-. shrs] [] map (personFollowers . entityVal) <$> selectList [PersonIdent <-. sids] [] getOtherFollowerSets :: (MonadSite m, YesodHashids (SiteEnv m)) => (ShrIdent, LocalSharerRelatedSet) -> ReaderT SqlBackend m [FollowerSetId] getOtherFollowerSets (shr, LocalSharerRelatedSet _ tickets patches projects repos) = do msid <- getKeyBy $ UniqueSharer shr case msid of Nothing -> return [] Just sid -> do mpid <- getKeyBy $ UniquePersonIdent sid (\ tp j r -> map E.unValue tp ++ j ++ r) <$> case mpid of Nothing -> pure [] Just pid -> getSharerTicketFollowerSets pid tickets patches <*> getProjectFollowerSets sid projects <*> getRepoFollowerSets sid repos where getSharerTicketFollowerSets pid tickets patches = do let talkhids = [talkhid | (talkhid, t) <- tickets , localRecipTicketFollowers t ] ++ [talkhid | (talkhid, p) <- patches , localRecipPatchFollowers p ] 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 || isAuthor (LocalActorProject shr prj)) ] fsidsJ <- map (projectFollowers . entityVal) <$> selectList [ProjectSharer ==. sid, ProjectIdent <-. prjsJ] [] let prjsT = if requireOwner then [ (prj, localRecipProjectTicketRelated j) | (prj, j) <- projects , localRecipProject (localRecipProjectDirect j) || isAuthor (LocalActorProject shr prj) ] 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` tcl `E.InnerJoin` tpl `E.LeftOuterJoin` tup `E.LeftOuterJoin` tar) -> do E.on $ E.just (tcl E.^. TicketContextLocalId) E.==. tar E.?. TicketAuthorRemoteTicket E.on $ E.just (tcl E.^. TicketContextLocalId) E.==. tup E.?. TicketUnderProjectProject E.on $ tcl E.^. TicketContextLocalId E.==. tpl E.^. TicketProjectLocalContext E.on $ t E.^. TicketId E.==. tcl E.^. TicketContextLocalTicket 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 = do let rpsR = [rp | (rp, r) <- repos , let d = localRecipRepoDirect r in localRecipRepoFollowers d && (localRecipRepo d || not requireOwner || isAuthor (LocalActorRepo shr rp)) ] fsidsR <- map (repoFollowers . entityVal) <$> selectList [RepoSharer ==. sid, RepoIdent <-. rpsR] [] let rpsP = if requireOwner then [ (rp, localRecipRepoPatchRelated r) | (rp, r) <- repos , localRecipRepo (localRecipRepoDirect r) || isAuthor (LocalActorRepo shr rp) ] else map (second localRecipRepoPatchRelated) repos fsidssP <- for rpsP $ \ (rp, patches) -> do mrid <- getKeyBy $ UniqueRepo rp sid case mrid of Nothing -> return [] Just rid -> getPatchFollowerSets rid patches return $ fsidsR ++ map E.unValue (concat fsidssP) where getPatchFollowerSets rid patches = do let ltkhids = [ltkhid | (ltkhid, p) <- patches , localRecipPatchFollowers p ] ltids <- catMaybes <$> traverse decodeKeyHashid ltkhids E.select $ E.from $ \ (lt `E.InnerJoin` t `E.InnerJoin` tcl `E.InnerJoin` trl `E.LeftOuterJoin` tup `E.LeftOuterJoin` tar) -> do E.on $ E.just (tcl E.^. TicketContextLocalId) E.==. tar E.?. TicketAuthorRemoteTicket E.on $ E.just (tcl E.^. TicketContextLocalId) E.==. tup E.?. TicketUnderProjectProject E.on $ tcl E.^. TicketContextLocalId E.==. trl E.^. TicketRepoLocalContext E.on $ t E.^. TicketId E.==. tcl E.^. TicketContextLocalTicket E.on $ lt E.^. LocalTicketTicket E.==. t E.^. TicketId E.where_ $ trl E.^. TicketRepoLocalRepo E.==. E.val rid E.&&. E.not_ ( E.isNothing (tup E.?. TicketUnderProjectId) E.&&. E.isNothing (tar E.?. TicketAuthorRemoteId) ) return $ lt E.^. LocalTicketFollowers getLocalFollowers :: MonadIO m => [FollowerSetId] -> ReaderT SqlBackend m [InboxId] getLocalFollowers fsids = do pids <- map (followPerson . entityVal) <$> selectList [FollowTarget <-. fsids] [] map (personInbox . entityVal) <$> selectList [PersonId <-. pids] [Asc PersonInbox] getRemoteFollowers :: MonadIO m => [FollowerSetId] -> ReaderT SqlBackend m [((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 :: MonadIO m => (ShrIdent, LocalSharerRelatedSet) -> ReaderT SqlBackend m [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 || isAuthor (LocalActorProject shr prj)) && (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 || isAuthor (LocalActorRepo shr rp)) ] rids <- selectKeysList [RepoSharer ==. sid, RepoIdent <-. rps] [] pids <- map (repoCollabPerson . entityVal) <$> selectList [RepoCollabRepo <-. rids] [] map (personInbox . entityVal) <$> selectList [PersonId <-. pids] [Asc PersonInbox] -- | 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' :: (MonadSite m, YesodHashids (SiteEnv m)) => Bool -- ^ Whether to deliver to collection only if owner actor is addressed -> LocalActor -> InboxId -> OutboxItemId -> LocalRecipientSet -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty RemoteRecipient)] deliverLocal' requireOwner author ibidAuthor obiid = insertActivityToLocalInboxes makeItem requireOwner (Just author) (Just ibidAuthor) where makeItem ibid ibiid = InboxItemLocal ibid obiid ibiid insertRemoteActivityToLocalInboxes :: (MonadSite m, YesodHashids (SiteEnv m)) => Bool -> RemoteActivityId -> LocalRecipientSet -> ReaderT SqlBackend m [((InstanceId, Host), NonEmpty RemoteRecipient)] insertRemoteActivityToLocalInboxes requireOwner ractid = insertActivityToLocalInboxes makeItem requireOwner Nothing Nothing where makeItem ibid ibiid = InboxItemRemote ibid ractid ibiid provideEmptyCollection :: CollectionType -> Route App -> Handler TypedContent provideEmptyCollection typ here = do encodeRouteLocal <- getEncodeRouteLocal let coll = Collection { collectionId = encodeRouteLocal here , collectionType = typ , collectionTotalItems = Just 0 , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = [] :: [Text] } provideHtmlAndAP coll $ redirectToPrettyJSON here insertEmptyOutboxItem obid now = do h <- asksSite siteInstanceHost insert OutboxItem { outboxItemOutbox = obid , outboxItemActivity = persistJSONObjectFromDoc $ Doc h emptyActivity , outboxItemPublished = now }