1
0
Fork 0
mirror of https://code.naskya.net/repos/ndqEd synced 2025-01-27 18:07:50 +09:00
vervis/src/Vervis/API.hs
2020-05-14 08:59:34 +00:00

1402 lines
65 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.API
( noteC
, createNoteC
, createTicketC
, followC
, offerTicketC
, undoC
, pushCommitsC
, getFollowersCollection
)
where
import Control.Applicative
import Control.Concurrent.MVar
import Control.Concurrent.STM.TVar
import Control.Exception hiding (Handler, try)
import Control.Monad
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader
import Crypto.Hash
import Data.Aeson
import Data.Bifunctor
import Data.ByteString (ByteString)
import Data.Either
import Data.Foldable
import Data.Function
import Data.List (sort, deleteBy, nub, union, unionBy, partition)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import Data.Maybe
import Data.Semigroup
import Data.Text (Text)
import Data.Text.Encoding
import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.Units
import Data.Traversable
import Data.Tuple
import Database.Persist hiding (deleteBy)
import Database.Persist.Sql hiding (deleteBy)
import Network.HTTP.Client
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI
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.Persist.Core
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
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 qualified Network.Wai as W
import Data.Time.Interval
import Network.HTTP.Signature hiding (requestHeaders)
import Yesod.HttpSignature
import Crypto.PublicVerifKey
import Database.Persist.JSON
import Network.FedURI
import Network.HTTP.Digest
import Web.ActivityPub hiding (Follow, Ticket)
import Yesod.ActivityPub
import Yesod.Auth.Unverified
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite
import qualified Web.ActivityPub as AP
import Control.Monad.Trans.Except.Local
import Data.Aeson.Local
import Data.Either.Local
import Data.List.Local
import Data.List.NonEmpty.Local
import Data.Maybe.Local
import Data.Tuple.Local
import Database.Persist.Local
import Yesod.Persist.Local
import Vervis.ActivityPub
import Vervis.ActivityPub.Recipient
import Vervis.ActorKey
import Vervis.FedURI
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Model.Ticket
import Vervis.RemoteActorStore
import Vervis.Settings
import Vervis.Ticket
verifyIsLoggedInUser
:: LocalURI
-> Text
-> ExceptT Text AppDB (PersonId, OutboxId, ShrIdent)
verifyIsLoggedInUser lu t = do
Entity pid p <- requireVerifiedAuth
s <- lift $ getJust $ personIdent p
route2local <- getEncodeRouteLocal
let shr = sharerIdent s
if route2local (SharerR shr) == lu
then return (pid, personOutbox p, shr)
else throwE t
verifyAuthor
:: ShrIdent
-> LocalURI
-> Text
-> ExceptT Text AppDB (PersonId, OutboxId)
verifyAuthor shr lu t = ExceptT $ do
Entity sid s <- getBy404 $ UniqueSharer shr
Entity pid p <- getBy404 $ UniquePersonIdent sid
encodeRouteLocal <- getEncodeRouteLocal
return $
if encodeRouteLocal (SharerR shr) == lu
then Right (pid, personOutbox p)
else Left t
parseComment :: LocalURI -> ExceptT Text Handler (ShrIdent, LocalMessageId)
parseComment luParent = do
route <- case decodeRouteLocal luParent of
Nothing -> throwE "Not a local route"
Just r -> return r
case route of
MessageR shr hid -> (shr,) <$> decodeKeyHashidE hid "Non-existent local message hashid"
_ -> throwE "Not a local message route"
noteC
:: Entity Person
-> Sharer
-> Note URIMode
-> Handler (Either Text LocalMessageId)
noteC person sharer note = do
let shrUser = sharerIdent sharer
summary <-
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
<a href=@{SharerR shrUser}>#{shr2text shrUser}
$maybe uContext <- noteContext note
\ commented under a #
<a href="#{renderObjURI uContext}">topic</a>.
$nothing
\ commented.
|]
createNoteC person sharer summary (noteAudience note) note
-- | Handle a Note submitted by a local user to their outbox. It can be either
-- a comment on a local ticket, or a comment on some remote context. Return an
-- error message if the Note is rejected, otherwise the new 'LocalMessageId'.
createNoteC
:: Entity Person
-> Sharer
-> TextHtml
-> Audience URIMode
-> Note URIMode
-> Handler (Either Text LocalMessageId)
createNoteC (Entity pidUser personUser) sharerUser summary audience note = runExceptT $ do
let shrUser = sharerIdent sharerUser
noteData@(muParent, mparent, uContext, context, source, content) <- checkNote shrUser note
(localRecips, remoteRecips) <- do
mrecips <- parseAudience audience
fromMaybeE mrecips "Create Note with no recipients"
checkFederation remoteRecips
verifyContextRecip context localRecips remoteRecips
now <- liftIO getCurrentTime
(lmid, obiid, doc, remotesHttp) <- runDBExcept $ do
obiidCreate <- lift $ insertEmptyOutboxItem (personOutbox personUser) now
(mproject, did, meparent) <- getTopicAndParent context mparent
lmid <- lift $ insertMessage now content source obiidCreate did meparent
docCreate <- lift $ insertCreateToOutbox now shrUser noteData obiidCreate lmid
remoteRecipsHttpCreate <- do
hashLT <- getEncodeKeyHashid
hashTAL <- getEncodeKeyHashid
let sieve =
let actors =
case mproject of
Nothing -> []
Just (shr, prj) -> [LocalActorProject shr prj]
collections =
let project =
case mproject of
Nothing -> []
Just (shr, prj) ->
[ LocalPersonCollectionProjectTeam shr prj
, LocalPersonCollectionProjectFollowers shr prj
]
ticket =
case context of
Left nc ->
case nc of
NoteContextSharerTicket shr talid ->
let talkhid = hashTAL talid
in [ -- LocalPersonCollectionSharerTicketTeam shr talkhid
LocalPersonCollectionSharerTicketFollowers shr talkhid
]
NoteContextProjectTicket shr prj ltid ->
let ltkhid = hashLT ltid
in [ -- LocalPersonCollectionProjectTicketTeam shr prj ltkhid
LocalPersonCollectionProjectTicketFollowers shr prj ltkhid
]
Right _ -> []
commenter = [LocalPersonCollectionSharerFollowers shrUser]
in project ++ ticket ++ commenter
in makeRecipientSet actors collections
moreRemoteRecips <-
lift $ deliverLocal' True (LocalActorSharer shrUser) (personInbox personUser) obiidCreate $
localRecipSieve' sieve True False localRecips
checkFederation moreRemoteRecips
lift $ deliverRemoteDB' (objUriAuthority uContext) obiidCreate remoteRecips moreRemoteRecips
return (lmid, obiidCreate, docCreate, remoteRecipsHttpCreate)
lift $ forkWorker "createNoteC: async HTTP delivery" $ deliverRemoteHttp (objUriAuthority uContext) obiid doc remotesHttp
return lmid
where
checkNote shrUser (Note mluNote luAttrib _aud muParent muContext mpublished source content) = do
verifyNothingE mluNote "Note specifies an id"
encodeRouteLocal <- getEncodeRouteLocal
unless (encodeRouteLocal (SharerR shrUser) == luAttrib) $
throwE "Note attributed to someone else"
verifyNothingE mpublished "Note specifies published"
uContext <- fromMaybeE muContext "Note without context"
context <- parseNoteContext uContext
mparent <- checkParent context =<< traverse parseParent muParent
return (muParent, mparent, uContext, context, source, content)
where
parseTopic name route =
case route of
SharerTicketR shr talkhid ->
NoteContextSharerTicket shr <$>
decodeKeyHashidE
talkhid
(name <> " sharer ticket invalid talkhid")
ProjectTicketR shr prj ltkhid ->
NoteContextProjectTicket shr prj <$>
decodeKeyHashidE
ltkhid
(name <> " project ticket invalid ltkhid")
_ -> throwE $ name <> " isn't a discussion topic route"
parseNoteContext u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <-
fromMaybeE
(decodeRouteLocal lu)
"Note context local but not a valid route"
parseTopic "Note context" route
else return $ Right u
parseParent u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <-
fromMaybeE
(decodeRouteLocal lu)
"Note parent local but not a valid route"
Left <$> parseTopic "Note parent" route <|>
Right <$> parseComment route
else return $ Right u
where
parseComment (MessageR shr lmkhid) =
(shr,) <$> decodeKeyHashidE lmkhid "Note parent invalid lmkhid"
parseComment _ = throwE "Note parent not a comment route"
checkParent _ Nothing = return Nothing
checkParent (Left topic) (Just (Left (Left topic'))) =
if topic == topic'
then return Nothing
else throwE "Note context and parent are different local topics"
checkParent _ (Just (Left (Right msg))) = return $ Just $ Left msg
checkParent (Left _) (Just (Right u)) = return $ Just $ Right u
checkParent (Right u) (Just (Right u')) =
return $
if u == u'
then Nothing
else Just $ Right u'
checkFederation remoteRecips = do
federation <- asksSite $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients found"
verifyContextRecip (Right (ObjURI h _)) _ remoteRecips =
unless (any ((== h) . fst) remoteRecips) $
throwE
"Context is remote but no recipients of that host are listed"
verifyContextRecip (Left (NoteContextSharerTicket shr _)) localRecips _ =
fromMaybeE
verify
"Local context ticket's hosting sharer isn't listed as a recipient"
where
verify = do
sharerSet <- lookup shr localRecips
guard $ localRecipSharer $ localRecipSharerDirect sharerSet
verifyContextRecip (Left (NoteContextProjectTicket shr prj _)) localRecips _ =
fromMaybeE
verify
"Local context ticket's hosting project isn't listed as a recipient"
where
verify = do
sharerSet <- lookup shr localRecips
projectSet <- lookup prj $ localRecipProjectRelated sharerSet
guard $ localRecipProject $ localRecipProjectDirect projectSet
insertEmptyOutboxItem obid now = do
h <- asksSite siteInstanceHost
insert OutboxItem
{ outboxItemOutbox = obid
, outboxItemActivity = persistJSONObjectFromDoc $ Doc h emptyActivity
, outboxItemPublished = now
}
getProject tpl = do
j <- getJust $ ticketProjectLocalProject tpl
s <- getJust $ projectSharer j
return (sharerIdent s, projectIdent j)
getTopicAndParent (Left context) mparent = do
(mproject, did) <-
case context of
NoteContextSharerTicket shr talid -> do
(_, Entity _ lt, _, project) <- do
mticket <- lift $ getSharerTicket shr talid
fromMaybeE mticket "Note context no such local sharer-hosted ticket"
mproj <-
case project of
Left (Entity _ tpl) -> lift $ Just <$> getProject tpl
Right _ -> return Nothing
return (mproj, localTicketDiscuss lt)
NoteContextProjectTicket shr prj ltid -> do
(_, _, _, Entity _ lt, _, _) <- do
mticket <- lift $ getProjectTicket shr prj ltid
fromMaybeE mticket "Note context no such local project-hosted ticket"
return (Just (shr, prj), localTicketDiscuss lt)
mmidParent <- for mparent $ \ parent ->
case parent of
Left (shrParent, lmidParent) -> getLocalParentMessageId did shrParent lmidParent
Right (ObjURI hParent luParent) -> do
mrm <- lift $ runMaybeT $ do
iid <- MaybeT $ getKeyBy $ UniqueInstance hParent
roid <- MaybeT $ getKeyBy $ UniqueRemoteObject iid luParent
MaybeT $ getValBy $ UniqueRemoteMessageIdent roid
rm <- fromMaybeE mrm "Remote parent unknown locally"
let mid = remoteMessageRest rm
m <- lift $ getJust mid
unless (messageRoot m == did) $
throwE "Remote parent belongs to a different discussion"
return mid
return (mproject, did, Left <$> mmidParent)
getTopicAndParent (Right u@(ObjURI h lu)) mparent = do
(mproject, rd, rdnew) <- lift $ do
iid <- either entityKey id <$> insertBy' (Instance h)
roid <- either entityKey id <$> insertBy' (RemoteObject iid lu)
merd <- getBy $ UniqueRemoteDiscussionIdent roid
case merd of
Just (Entity rdid rd) -> do
mproj <- do
mrt <- getValBy $ UniqueRemoteTicketDiscuss rdid
for mrt $ \ rt -> do
tar <- getJust $ remoteTicketTicket rt
tpl <- getJust $ ticketAuthorRemoteTicket tar
getProject tpl
return (mproj, rd, False)
Nothing -> do
did <- insert Discussion
(rd, rdnew) <- valAndNew <$> insertByEntity' (RemoteDiscussion roid did)
unless rdnew $ delete did
return (Nothing, rd, rdnew)
let did = remoteDiscussionDiscuss rd
meparent <- for mparent $ \ parent ->
case parent of
Left (shrParent, lmidParent) -> do
when rdnew $ throwE "Local parent inexistent, RemoteDiscussion is new"
Left <$> getLocalParentMessageId did shrParent lmidParent
Right uParent@(ObjURI hParent luParent) -> do
mrm <- lift $ runMaybeT $ do
iid <- MaybeT $ getKeyBy $ UniqueInstance hParent
roid <- MaybeT $ getKeyBy $ UniqueRemoteObject iid luParent
MaybeT $ getValBy $ UniqueRemoteMessageIdent roid
case mrm of
Nothing -> return $ Right uParent
Just rm -> Left <$> do
let mid = remoteMessageRest rm
m <- lift $ getJust mid
unless (messageRoot m == did) $
throwE "Remote parent belongs to a different discussion"
return mid
return (mproject, did, meparent)
insertMessage now content source obiidCreate did meparent = do
mid <- insert Message
{ messageCreated = now
, messageSource = source
, messageContent = content
, messageParent =
case meparent of
Just (Left midParent) -> Just midParent
_ -> Nothing
, messageRoot = did
}
insert LocalMessage
{ localMessageAuthor = pidUser
, localMessageRest = mid
, localMessageCreate = obiidCreate
, localMessageUnlinkedParent =
case meparent of
Just (Right uParent) -> Just uParent
_ -> Nothing
}
insertCreateToOutbox now shrUser (muParent, _mparent, uContext, _context, source, content) obiidCreate lmid = do
encodeRouteLocal <- getEncodeRouteLocal
hLocal <- asksSite siteInstanceHost
obikhid <- encodeKeyHashid obiidCreate
lmkhid <- encodeKeyHashid lmid
let luAttrib = encodeRouteLocal $ SharerR shrUser
create = Doc hLocal Activity
{ activityId = Just $ encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
, activityActor = luAttrib
, activitySummary = Just summary
, activityAudience = audience
, activitySpecific = CreateActivity Create
{ createObject = CreateNote Note
{ noteId = Just $ encodeRouteLocal $ MessageR shrUser lmkhid
, noteAttrib = luAttrib
, noteAudience = emptyAudience
, noteReplyTo = Just $ fromMaybe uContext muParent
, noteContext = Just uContext
, notePublished = Just now
, noteSource = source
, noteContent = content
}
, createTarget = Nothing
}
}
update obiidCreate [OutboxItemActivity =. persistJSONObjectFromDoc create]
return create
-- | Handle a Ticket submitted by a local user to their outbox. The ticket's
-- context project may be local or remote. Return an error message if the
-- Ticket is rejected, otherwise the new 'TicketAuthorLocalId'.
createTicketC
:: Entity Person
-> Sharer
-> TextHtml
-> Audience URIMode
-> AP.Ticket URIMode
-> Maybe FedURI
-> Handler (Either Text TicketAuthorLocalId)
createTicketC (Entity pidUser personUser) sharerUser summary audience ticket muTarget = runExceptT $ do
let shrUser = sharerIdent sharerUser
ticketData@(uContext, title, desc, source, uTarget) <- checkTicket shrUser ticket muTarget
context <- parseTicketContext uContext
(localRecips, remoteRecips) <- do
mrecips <- parseAudience audience
fromMaybeE mrecips "Create Ticket with no recipients"
checkFederation remoteRecips
verifyProjectRecip context localRecips
tracker <- fetchTracker context uTarget
now <- liftIO getCurrentTime
(talid, obiidCreate, docCreate, remotesHttpCreate, maybeAccept) <- runDBExcept $ do
obiidCreate <- lift $ insertEmptyOutboxItem (personOutbox personUser) now
project <- prepareProject now tracker
talid <- lift $ insertTicket now pidUser title desc source obiidCreate project
docCreate <- lift $ insertCreateToOutbox shrUser ticketData now obiidCreate talid
remoteRecipsHttpCreate <- do
let sieve =
case tracker of
Left (shr, prj) ->
makeRecipientSet
[ LocalActorProject shr prj
]
[ LocalPersonCollectionSharerFollowers shrUser
, LocalPersonCollectionProjectTeam shr prj
, LocalPersonCollectionProjectFollowers shr prj
]
Right _ ->
makeRecipientSet
[]
[LocalPersonCollectionSharerFollowers shrUser]
moreRemoteRecips <- lift $ deliverLocal' True (LocalActorSharer shrUser) (personInbox personUser) obiidCreate $ localRecipSieve sieve False localRecips
checkFederation moreRemoteRecips
lift $ deliverRemoteDB' (objUriAuthority uTarget) obiidCreate remoteRecips moreRemoteRecips
maccept <-
case project of
Left proj@(shr, Entity _ j, obiidAccept) -> Just <$> do
let prj = projectIdent j
recipsA =
[ LocalActorSharer shrUser
]
recipsC =
[ LocalPersonCollectionProjectTeam shr prj
, LocalPersonCollectionProjectFollowers shr prj
, LocalPersonCollectionSharerFollowers shrUser
]
doc <- lift $ insertAcceptToOutbox proj shrUser obiidCreate talid recipsA recipsC
recips <- lift $ deliverLocal' True (LocalActorProject shr prj) (projectInbox j) obiidAccept $ makeRecipientSet recipsA recipsC
checkFederation recips
lift $ (obiidAccept,doc,) <$> deliverRemoteDB' dont obiidAccept [] recips
Right _ -> return Nothing
return (talid, obiidCreate, docCreate, remoteRecipsHttpCreate, maccept)
lift $ do
forkWorker "createTicketC: async HTTP Create delivery" $ deliverRemoteHttp (objUriAuthority uTarget) obiidCreate docCreate remotesHttpCreate
for_ maybeAccept $ \ (obiidAccept, docAccept, remotesHttpAccept) ->
forkWorker "createTicketC: async HTTP Accept delivery" $ deliverRemoteHttp dont obiidAccept docAccept remotesHttpAccept
return talid
where
checkTicket shr (AP.Ticket mlocal luAttrib mpublished mupdated mcontext summary content source massigned resolved) mtarget = do
verifyNothingE mlocal "Ticket with 'id'"
encodeRouteLocal <- getEncodeRouteLocal
unless (encodeRouteLocal (SharerR shr) == luAttrib) $
throwE "Ticket attributed to someone else"
verifyNothingE mpublished "Ticket with 'published'"
verifyNothingE mupdated "Ticket with 'updated'"
context <- fromMaybeE mcontext "Ticket without 'context'"
verifyNothingE massigned "Ticket with 'assignedTo'"
when resolved $ throwE "Ticket resolved"
target <- fromMaybeE mtarget "Create Ticket without 'target'"
return (context, summary, content, source, target)
parseTicketContext :: (MonadSite m, SiteEnv m ~ App) => FedURI -> ExceptT Text m (Either (ShrIdent, PrjIdent) FedURI)
parseTicketContext u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <- fromMaybeE (decodeRouteLocal lu) "Ticket context isn't a valid route"
case route of
ProjectR shr prj -> return (shr, prj)
_ -> throwE "Ticket context isn't a project route"
else return $ Right u
checkFederation remoteRecips = do
federation <- asksSite $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients found"
verifyProjectRecip (Right _) _ = return ()
verifyProjectRecip (Left (shr, prj)) localRecips =
fromMaybeE verify "Local context project isn't listed as a recipient"
where
verify = do
sharerSet <- lookup shr localRecips
projectSet <- lookup prj $ localRecipProjectRelated sharerSet
guard $ localRecipProject $ localRecipProjectDirect projectSet
fetchTracker c u@(ObjURI h lu) = do
hl <- hostIsLocal h
case (hl, c) of
(True, Left (shr, prj)) -> Left <$> do
encodeRouteLocal <- getEncodeRouteLocal
unless (encodeRouteLocal (ProjectR shr prj) == lu) $
throwE "Local context and target mismatch"
return (shr, prj)
(True, Right _) -> throwE "context and target different host"
(False, Left _) -> throwE "context and target different host"
(False, Right (ObjURI h' lu')) -> Right <$> do
unless (h == h') $ throwE "context and target different host"
(iid, era) <- do
iid <- lift $ runDB $ either entityKey id <$> insertBy' (Instance h)
result <- lift $ fetchRemoteActor iid h lu
case result of
Left e -> throwE $ T.pack $ displayException e
Right (Left e) -> throwE $ T.pack $ show e
Right (Right mera) -> do
era <- fromMaybeE mera "target found to be a collection, not an actor"
return (iid, era)
return (iid, era, if lu == lu' then Nothing else Just lu')
insertEmptyOutboxItem obid now = do
h <- asksSite siteInstanceHost
insert OutboxItem
{ outboxItemOutbox = obid
, outboxItemActivity = persistJSONObjectFromDoc $ Doc h emptyActivity
, outboxItemPublished = now
}
prepareProject now (Left (shr, prj)) = Left <$> do
mej <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
MaybeT $ getBy $ UniqueProject prj sid
ej@(Entity _ j) <- fromMaybeE mej "Local context: no such project"
obiidAccept <- lift $ insertEmptyOutboxItem (projectOutbox j) now
return (shr, ej, obiidAccept)
prepareProject _ (Right (iid, era, mlu)) = lift $ Right <$> do
mroid <- for mlu $ \ lu -> either entityKey id <$> insertBy' (RemoteObject iid lu)
return (era, mroid)
insertTicket now pidUser title desc source obiidCreate project = do
did <- insert Discussion
fsid <- insert FollowerSet
tid <- insert Ticket
{ ticketNumber = Nothing
, ticketCreated = now
, ticketTitle = unTextHtml title
, ticketSource = unTextPandocMarkdown source
, ticketDescription = unTextHtml desc
, ticketAssignee = Nothing
, ticketStatus = TSNew
, ticketClosed = UTCTime (ModifiedJulianDay 0) 0
, ticketCloser = Nothing
}
ltid <- insert LocalTicket
{ localTicketTicket = tid
, localTicketDiscuss = did
, localTicketFollowers = fsid
}
talid <- insert TicketAuthorLocal
{ ticketAuthorLocalTicket = ltid
, ticketAuthorLocalAuthor = pidUser
, ticketAuthorLocalOpen = obiidCreate
}
case project of
Left (_shr, Entity jid _j, obiidAccept) ->
insert_ TicketProjectLocal
{ ticketProjectLocalTicket = tid
, ticketProjectLocalProject = jid
, ticketProjectLocalAccept = obiidAccept
}
Right (Entity raid _ra, mroid) ->
insert_ TicketProjectRemote
{ ticketProjectRemoteTicket = talid
, ticketProjectRemoteTracker = raid
, ticketProjectRemoteProject = mroid
}
return talid
insertCreateToOutbox shrUser (uContext, title, desc, source, uTarget) now obiidCreate talid = do
encodeRouteLocal <- getEncodeRouteLocal
hLocal <- asksSite siteInstanceHost
talkhid <- encodeKeyHashid talid
obikhid <- encodeKeyHashid obiidCreate
let luAttrib = encodeRouteLocal $ SharerR shrUser
tlocal = TicketLocal
{ ticketId = encodeRouteLocal $ SharerTicketR shrUser talkhid
, ticketReplies = encodeRouteLocal $ SharerTicketDiscussionR shrUser talkhid
, ticketParticipants = encodeRouteLocal $ SharerTicketFollowersR shrUser talkhid
, ticketTeam = encodeRouteLocal $ SharerTicketTeamR shrUser talkhid
, ticketEvents = encodeRouteLocal $ SharerTicketEventsR shrUser talkhid
, ticketDeps = encodeRouteLocal $ SharerTicketDepsR shrUser talkhid
, ticketReverseDeps = encodeRouteLocal $ SharerTicketReverseDepsR shrUser talkhid
}
create = Doc hLocal Activity
{ activityId = Just $ encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
, activityActor = luAttrib
, activitySummary = Just summary
, activityAudience = audience
, activitySpecific = CreateActivity Create
{ createObject = CreateTicket AP.Ticket
{ AP.ticketLocal = Just (hLocal, tlocal)
, AP.ticketAttributedTo = luAttrib
, AP.ticketPublished = Just now
, AP.ticketUpdated = Nothing
, AP.ticketContext = Just uContext
, AP.ticketSummary = title
, AP.ticketContent = desc
, AP.ticketSource = source
, AP.ticketAssignedTo = Nothing
, AP.ticketIsResolved = False
}
, createTarget = Just uTarget
}
}
update obiidCreate [OutboxItemActivity =. persistJSONObjectFromDoc create]
return create
insertAcceptToOutbox (shrJ, Entity _ j, obiidAccept) shrU obiidCreate talid actors colls = do
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
hLocal <- asksSite siteInstanceHost
obikhidAccept <- encodeKeyHashid obiidAccept
obikhidCreate <- encodeKeyHashid obiidCreate
talkhid <- encodeKeyHashid talid
let prjJ = projectIdent j
summary <-
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
Project #
<a href=@{ProjectR shrJ prjJ}>
#{prj2text prjJ}
\ accepted #
<a href=@{SharerTicketR shrU talkhid}>
ticket
\ by #
<a href=@{SharerR shrU}>
#{shr2text shrU}
|]
let recips = map encodeRouteHome $ map renderLocalActor actors ++ map renderLocalPersonCollection colls
accept = Doc hLocal Activity
{ activityId = Just $ encodeRouteLocal $ ProjectOutboxItemR shrJ prjJ obikhidAccept
, activityActor = encodeRouteLocal $ ProjectR shrJ prjJ
, activitySummary = Just summary
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject = encodeRouteHome $ SharerOutboxItemR shrU obikhidCreate
, acceptResult = Nothing
}
}
update obiidAccept [OutboxItemActivity =. persistJSONObjectFromDoc accept]
return accept
dont = Authority "dont-do.any-forwarding" Nothing
data Followee
= FolloweeSharer ShrIdent
| FolloweeSharerTicket ShrIdent (KeyHashid TicketAuthorLocal)
| FolloweeProject ShrIdent PrjIdent
| FolloweeProjectTicket ShrIdent PrjIdent (KeyHashid LocalTicket)
| FolloweeRepo ShrIdent RpIdent
followC
:: ShrIdent
-> TextHtml
-> Audience URIMode
-> AP.Follow URIMode
-> Handler (Either Text OutboxItemId)
followC shrUser summary audience follow@(AP.Follow uObject muContext hide) = runExceptT $ do
(localRecips, remoteRecips) <- do
mrecips <- parseAudience audience
fromMaybeE mrecips "Follow with no recipients"
federation <- asksSite $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients specified"
mfollowee <- do
let ObjURI h luObject = uObject
local <- hostIsLocal h
if local
then Just <$> do
route <-
fromMaybeE
(decodeRouteLocal luObject)
"Follow object isn't a valid route"
followee <-
fromMaybeE
(parseFollowee route)
"Follow object isn't a followee route"
let actor = followeeActor followee
unless (actorRecips actor == localRecips) $
throwE "Follow object isn't the recipient"
case followee of
FolloweeSharer shr | shr == shrUser ->
throwE "User trying to follow themselves"
_ -> return ()
return (followee, actor)
else do
unless (null localRecips) $
throwE "Follow object is remote but local recips listed"
return Nothing
let dont = Authority "dont-do.any-forwarding" Nothing
(obiidFollow, doc, remotesHttp) <- runDBExcept $ do
Entity pidAuthor personAuthor <- lift $ getAuthor shrUser
let ibidAuthor = personInbox personAuthor
obidAuthor = personOutbox personAuthor
(obiidFollow, doc, luFollow) <- lift $ insertFollowToOutbox obidAuthor
case mfollowee of
Nothing -> lift $ insert_ $ FollowRemoteRequest pidAuthor uObject muContext (not hide) obiidFollow
Just (followee, actorRecip) -> do
(fsid, ibidRecip, unread, obidRecip) <- getFollowee followee
obiidAccept <- lift $ insertAcceptToOutbox luFollow actorRecip obidRecip
deliverFollowLocal pidAuthor fsid unread obiidFollow obiidAccept ibidRecip
lift $ deliverAcceptLocal obiidAccept ibidAuthor
remotesHttp <- lift $ deliverRemoteDB' dont obiidFollow remoteRecips []
return (obiidFollow, doc, remotesHttp)
lift $ forkWorker "Outbox POST handler: async HTTP delivery" $ deliverRemoteHttp dont obiidFollow doc remotesHttp
return obiidFollow
where
parseFollowee (SharerR shr) = Just $ FolloweeSharer shr
parseFollowee (SharerTicketR shr khid) = Just $ FolloweeSharerTicket shr khid
parseFollowee (ProjectR shr prj) = Just $ FolloweeProject shr prj
parseFollowee (ProjectTicketR shr prj num) = Just $ FolloweeProjectTicket shr prj num
parseFollowee (RepoR shr rp) = Just $ FolloweeRepo shr rp
parseFollowee _ = Nothing
followeeActor (FolloweeSharer shr) = LocalActorSharer shr
followeeActor (FolloweeSharerTicket shr _) = LocalActorSharer shr
followeeActor (FolloweeProject shr prj) = LocalActorProject shr prj
followeeActor (FolloweeProjectTicket shr prj _) = LocalActorProject shr prj
followeeActor (FolloweeRepo shr rp) = LocalActorRepo shr rp
getAuthor shr = do
sid <- getKeyBy404 $ UniqueSharer shr
getBy404 $ UniquePersonIdent sid
getFollowee (FolloweeSharer shr) = do
msid <- lift $ getKeyBy $ UniqueSharer shr
sid <- fromMaybeE msid "Follow object: No such sharer in DB"
mval <- runMaybeT
$ Left <$> MaybeT (lift $ getValBy $ UniquePersonIdent sid)
<|> Right <$> MaybeT (lift $ getValBy $ UniqueGroup sid)
val <-
fromMaybeE mval $
"Found non-person non-group sharer: " <> shr2text shr
case val of
Left person -> return (personFollowers person, personInbox person, True, personOutbox person)
Right _group -> throwE "Follow object is a group"
getFollowee (FolloweeSharerTicket shr talkhid) = do
mfollowee <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
Entity pid p <- MaybeT $ getBy $ UniquePersonIdent sid
talid <- decodeKeyHashidM talkhid
tal <- MaybeT $ get talid
guard $ ticketAuthorLocalAuthor tal == pid
mtup <- lift $ getBy $ UniqueTicketUnderProjectAuthor talid
guard $ isNothing mtup
lt <- lift $ getJust $ ticketAuthorLocalTicket tal
return (lt, p)
(lt, p) <- fromMaybeE mfollowee "Follow object: No such sharer ticket in DB"
return (localTicketFollowers lt, personInbox p, True, personOutbox p)
getFollowee (FolloweeProject shr prj) = do
mproject <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
MaybeT $ getValBy $ UniqueProject prj sid
project <- fromMaybeE mproject "Follow object: No such project in DB"
return (projectFollowers project, projectInbox project, False, projectOutbox project)
getFollowee (FolloweeProjectTicket shr prj ltkhid) = do
mproject <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
Entity jid project <- MaybeT $ getBy $ UniqueProject prj sid
ltid <- decodeKeyHashidM ltkhid
lticket <- MaybeT $ get ltid
tpl <-
MaybeT $ getValBy $
UniqueTicketProjectLocal $ localTicketTicket lticket
guard $ ticketProjectLocalProject tpl == jid
return (lticket, project)
(lticket, project) <- fromMaybeE mproject "Follow object: No such project ticket in DB"
return (localTicketFollowers lticket, projectInbox project, False, projectOutbox project)
getFollowee (FolloweeRepo shr rp) = do
mrepo <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
MaybeT $ getValBy $ UniqueRepo rp sid
repo <- fromMaybeE mrepo "Follow object: No such repo in DB"
return (repoFollowers repo, repoInbox repo, False, repoOutbox repo)
insertFollowToOutbox obid = do
hLocal <- asksSite siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
let activity mluAct = Doc hLocal Activity
{ activityId = mluAct
, activityActor = encodeRouteLocal $ SharerR shrUser
, activitySummary = Just summary
, activityAudience = audience
, activitySpecific = FollowActivity follow
}
now <- liftIO getCurrentTime
obiid <- insert OutboxItem
{ outboxItemOutbox = obid
, outboxItemActivity =
persistJSONObjectFromDoc $ activity Nothing
, outboxItemPublished = now
}
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
doc = activity $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (obiid, doc, luAct)
deliverFollowLocal pidAuthor fsid unread obiidF obiidA ibidRecip = do
mfid <- lift $ insertUnique $ Follow pidAuthor fsid (not hide) obiidF obiidA
_ <- fromMaybeE mfid "Already following this object"
ibiid <- lift $ insert $ InboxItem unread
lift $ insert_ $ InboxItemLocal ibidRecip obiidF ibiid
insertAcceptToOutbox luFollow actorRecip obidRecip = do
now <- liftIO getCurrentTime
summary <-
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
<a href=@{SharerR shrUser}>
#{shr2text shrUser}
's follow request accepted by #
<a href=#{renderObjURI uObject}>
#{localUriPath $ objUriLocal uObject}
|]
hLocal <- asksSite siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let recips = [encodeRouteHome $ SharerR shrUser]
accept mluAct = Doc hLocal Activity
{ activityId = mluAct
, activityActor = objUriLocal uObject
, activitySummary = Just summary
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hLocal luFollow
, acceptResult = Nothing
}
}
obiid <- insert OutboxItem
{ outboxItemOutbox = obidRecip
, outboxItemActivity =
persistJSONObjectFromDoc $ accept Nothing
, outboxItemPublished = now
}
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ actorOutboxItem actorRecip obikhid
doc = accept $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return obiid
where
actorOutboxItem (LocalActorSharer shr) = SharerOutboxItemR shr
actorOutboxItem (LocalActorProject shr prj) = ProjectOutboxItemR shr prj
actorOutboxItem (LocalActorRepo shr rp) = RepoOutboxItemR shr rp
deliverAcceptLocal obiidAccept ibidAuthor = do
ibiid <- insert $ InboxItem True
insert_ $ InboxItemLocal ibidAuthor obiidAccept ibiid
offerTicketC
:: ShrIdent
-> TextHtml
-> Audience URIMode
-> Offer URIMode
-> Handler (Either Text OutboxItemId)
offerTicketC shrUser summary audience offer@(Offer ticket uTarget) = runExceptT $ do
(hProject, shrProject, prjProject) <- parseTarget uTarget
{-deps <- -}
checkOffer hProject shrProject prjProject
(localRecips, remoteRecips) <- do
mrecips <- parseAudience audience
fromMaybeE mrecips "Offer with no recipients"
federation <- asksSite $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients specified"
checkRecips hProject shrProject prjProject localRecips
now <- liftIO getCurrentTime
(obiid, doc, remotesHttp) <- runDBExcept $ do
(pidAuthor, obidAuthor) <-
verifyAuthor
shrUser
(AP.ticketAttributedTo ticket)
"Ticket attributed to different actor"
mprojAndDeps <- do
targetIsLocal <- hostIsLocal hProject
if targetIsLocal
then Just <$> getProjectAndDeps shrProject prjProject {-deps-}
else return Nothing
(obiid, doc, luOffer) <- lift $ insertToOutbox now obidAuthor
moreRemotes <-
lift $ deliverLocal pidAuthor shrProject prjProject now mprojAndDeps obiid luOffer localRecips
unless (federation || null moreRemotes) $
throwE "Federation disabled but remote collection members found"
remotesHttp <- lift $ deliverRemoteDB' hProject obiid remoteRecips moreRemotes
return (obiid, doc, remotesHttp)
lift $ forkWorker "Outbox POST handler: async HTTP delivery" $ deliverRemoteHttp hProject obiid doc remotesHttp
return obiid
where
checkOffer hProject shrProject prjProject = do
verifyNothingE (AP.ticketLocal ticket) "Ticket with 'id'"
verifyNothingE (AP.ticketPublished ticket) "Ticket with 'published'"
verifyNothingE (AP.ticketUpdated ticket) "Ticket with 'updated'"
-- verifyNothingE (AP.ticketName ticket) "Ticket with 'name'"
verifyNothingE (AP.ticketAssignedTo ticket) "Ticket with 'assignedTo'"
when (AP.ticketIsResolved ticket) $ throwE "Ticket resolved"
checkRecips hProject shrProject prjProject localRecips = do
local <- hostIsLocal hProject
if local
then traverse (verifyOfferRecips shrProject prjProject) localRecips
else traverse (verifyOnlySharer . snd) localRecips
where
verifyOfferRecips shr prj (shr', lsrSet) =
if shr == shr'
then unless (lsrSet == offerRecips prj) $
throwE "Unexpected offer target recipient set"
else verifyOnlySharer lsrSet
where
offerRecips prj = LocalSharerRelatedSet
{ localRecipSharerDirect = LocalSharerDirectSet False False
, localRecipSharerTicketRelated = []
, localRecipProjectRelated =
[ ( prj
, LocalProjectRelatedSet
{ localRecipProjectDirect =
LocalProjectDirectSet True True True
, localRecipProjectTicketRelated = []
}
)
]
, localRecipRepoRelated = []
}
verifyOnlySharer lsrSet = do
unless (null $ localRecipProjectRelated lsrSet) $
throwE "Unexpected recipients unrelated to offer target"
unless (null $ localRecipRepoRelated lsrSet) $
throwE "Unexpected recipients unrelated to offer target"
insertToOutbox now obid = do
hLocal <- asksSite siteInstanceHost
let activity mluAct = Doc hLocal Activity
{ activityId = mluAct
, activityActor = AP.ticketAttributedTo ticket
, activitySummary = Just summary
, activityAudience = audience
, activitySpecific = OfferActivity offer
}
obiid <- insert OutboxItem
{ outboxItemOutbox = obid
, outboxItemActivity =
persistJSONObjectFromDoc $ activity Nothing
, outboxItemPublished = now
}
encodeRouteLocal <- getEncodeRouteLocal
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
doc = activity $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (obiid, doc, luAct)
deliverLocal pidAuthor shrProject prjProject now mprojAndDeps obiid luOffer recips = do
(pids, remotes) <- forCollect recips $ \ (shr, LocalSharerRelatedSet sharer _ projects _) -> do
(pids, remotes) <-
traverseCollect (uncurry $ deliverLocalProject shr) projects
pids' <- do
mpid <-
if localRecipSharer sharer
then runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
MaybeT $ getKeyBy $ UniquePersonIdent sid
else return Nothing
return $
case mpid of
Nothing -> pids
Just pid -> LO.insertSet pid pids
return (pids', remotes)
for_ (L.delete pidAuthor pids) $ \ pid -> do
ibid <- personInbox <$> getJust pid
ibiid <- insert $ InboxItem True
insert_ $ InboxItemLocal ibid obiid ibiid
return remotes
where
traverseCollect action values =
bimap collectPids collectRemotes . unzip <$> traverse action values
where
collectPids = foldl' LO.union []
collectRemotes = foldl' unionRemotes []
forCollect = flip traverseCollect
deliverLocalProject shr prj (LocalProjectRelatedSet project _) =
case mprojAndDeps of
Just (sid, jid, ibid, fsid{-, tids-})
| shr == shrProject &&
prj == prjProject &&
localRecipProject project -> do
insertToInbox ibid
{-
num <-
((subtract 1) . projectNextTicket) <$>
updateGet jid [ProjectNextTicket +=. 1]
-}
obiidAccept <- do
obidProject <- projectOutbox <$> getJust jid
now <- liftIO getCurrentTime
hLocal <- asksSite siteInstanceHost
insert OutboxItem
{ outboxItemOutbox = obidProject
, outboxItemActivity =
persistJSONObjectFromDoc $ Doc hLocal emptyActivity
, outboxItemPublished = now
}
ltid <- insertTicket jid {-tids-} {-num-} obiidAccept
docAccept <- insertAccept pidAuthor sid jid fsid luOffer obiidAccept ltid
publishAccept pidAuthor sid jid fsid luOffer {-num-} obiidAccept docAccept
(pidsTeam, remotesTeam) <-
if localRecipProjectTeam project
then getProjectTeam sid
else return ([], [])
(pidsFollowers, remotesFollowers) <-
if localRecipProjectFollowers project
then getFollowers fsid
else return ([], [])
return
( LO.union pidsTeam pidsFollowers
, unionRemotes remotesTeam remotesFollowers
)
_ -> return ([], [])
where
insertToInbox ibid = do
ibiid <- insert $ InboxItem False
insert_ $ InboxItemLocal ibid obiid ibiid
insertAccept pidAuthor sid jid fsid luOffer obiid ltid = do
ltkhid <- encodeKeyHashid ltid
summary <-
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
<a href=@{SharerR shrUser}>
#{shr2text shrUser}
's ticket accepted by project #
<a href=@{ProjectR shrProject prjProject}>
./s/#{shr2text shrProject}/p/#{prj2text prjProject}
: #
<a href=@{ProjectTicketR shrProject prjProject ltkhid}>
#{preEscapedToHtml $ unTextHtml $ AP.ticketSummary ticket}.
|]
hLocal <- asksSite siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
obikhid <- encodeKeyHashid obiid
let recips =
map encodeRouteHome
[ SharerR shrUser
, ProjectTeamR shrProject prjProject
, ProjectFollowersR shrProject prjProject
]
doc = Doc hLocal Activity
{ activityId =
Just $ encodeRouteLocal $
ProjectOutboxItemR shrProject prjProject obikhid
, activityActor =
encodeRouteLocal $ ProjectR shrProject prjProject
, activitySummary = Just summary
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hLocal luOffer
, acceptResult =
Just $ encodeRouteLocal $
ProjectTicketR shrProject prjProject ltkhid
}
}
update
obiid
[OutboxItemActivity =. persistJSONObjectFromDoc doc]
return doc
insertTicket jid {-tidsDeps-} {-next-} obiidAccept = do
did <- insert Discussion
fsid <- insert FollowerSet
tid <- insert Ticket
{ ticketNumber = Nothing
, ticketCreated = now
, ticketTitle = unTextHtml $ AP.ticketSummary ticket
, ticketSource =
unTextPandocMarkdown $ AP.ticketSource ticket
, ticketDescription = unTextHtml $ AP.ticketContent ticket
, ticketAssignee = Nothing
, ticketStatus = TSNew
, ticketClosed = UTCTime (ModifiedJulianDay 0) 0
, ticketCloser = Nothing
}
ltid <- insert LocalTicket
{ localTicketTicket = tid
, localTicketDiscuss = did
, localTicketFollowers = fsid
}
tplid <- insert TicketProjectLocal
{ ticketProjectLocalTicket = tid
, ticketProjectLocalProject = jid
, ticketProjectLocalAccept = obiidAccept
}
talid <- insert TicketAuthorLocal
{ ticketAuthorLocalTicket = ltid
, ticketAuthorLocalAuthor = pidAuthor
, ticketAuthorLocalOpen = obiid
}
insert_ TicketUnderProject
{ ticketUnderProjectProject = tplid
, ticketUnderProjectAuthor = talid
}
--insertMany_ $ map (TicketDependency tid) tidsDeps
-- insert_ $ Follow pidAuthor fsid False True
return ltid
publishAccept pidAuthor sid jid fsid luOffer {-num-} obiid doc = do
now <- liftIO getCurrentTime
let dont = Authority "dont-do.any-forwarding" Nothing
remotesHttp <- do
moreRemotes <- deliverLocal now sid fsid obiid
deliverRemoteDB' dont obiid [] moreRemotes
site <- askSite
liftIO $ runWorker (deliverRemoteHttp dont obiid doc remotesHttp) site
where
deliverLocal now sid fsid obiid = do
(pidsTeam, remotesTeam) <- getProjectTeam sid
(pidsFollowers, remotesFollowers) <- getFollowers fsid
let pids = LO.insertSet pidAuthor $ LO.union pidsTeam pidsFollowers
remotes = unionRemotes remotesTeam remotesFollowers
for_ pids $ \ pid -> do
ibid <- personInbox <$> getJust pid
ibiid <- insert $ InboxItem True
insert_ $ InboxItemLocal ibid obiid ibiid
return remotes
undoC
:: ShrIdent
-> TextHtml
-> Audience URIMode
-> Undo URIMode
-> Handler (Either Text OutboxItemId)
undoC shrUser summary audience undo@(Undo luObject) = runExceptT $ do
(localRecips, remoteRecips) <- do
mrecips <- parseAudience audience
fromMaybeE mrecips "Follow with no recipients"
federation <- asksSite $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients specified"
route <-
fromMaybeE
(decodeRouteLocal luObject)
"Undo object isn't a valid route"
obiidOriginal <- case route of
SharerOutboxItemR shr obikhid
| shr == shrUser ->
decodeKeyHashidE obikhid "Undo object invalid obikhid"
_ -> throwE "Undo object isn't actor's outbox item route"
let dont = Authority "dont-do.any-forwarding" Nothing
(obiidUndo, doc, remotesHttp) <- runDBExcept $ do
Entity _pidAuthor personAuthor <- lift $ getAuthor shrUser
obi <- do
mobi <- lift $ get obiidOriginal
fromMaybeE mobi "Undo object obiid doesn't exist in DB"
unless (outboxItemOutbox obi == personOutbox personAuthor) $
throwE "Undo object obiid belongs to different actor"
lift $ do
deleteFollow obiidOriginal
deleteFollowRemote obiidOriginal
deleteFollowRemoteRequest obiidOriginal
let obidAuthor = personOutbox personAuthor
(obiidUndo, doc, luUndo) <- insertUndoToOutbox obidAuthor
let ibidAuthor = personInbox personAuthor
fsidAuthor = personFollowers personAuthor
knownRemotes <- deliverLocal shrUser ibidAuthor fsidAuthor obiidUndo localRecips
remotesHttp <- deliverRemoteDB' dont obiidUndo remoteRecips knownRemotes
return (obiidUndo, doc, remotesHttp)
lift $ forkWorker "undoC: Outbox POST handler: async HTTP delivery" $ deliverRemoteHttp dont obiidUndo doc remotesHttp
return obiidUndo
where
getAuthor shr = do
sid <- getKeyBy404 $ UniqueSharer shr
getBy404 $ UniquePersonIdent sid
deleteFollow obiid = do
mfid <- getKeyBy $ UniqueFollowFollow obiid
traverse_ delete mfid
deleteFollowRemote obiid = do
mfrid <- getKeyBy $ UniqueFollowRemoteFollow obiid
traverse_ delete mfrid
deleteFollowRemoteRequest obiid = do
mfrrid <- getKeyBy $ UniqueFollowRemoteRequestActivity obiid
traverse_ delete mfrrid
insertUndoToOutbox obid = do
hLocal <- asksSite siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
let activity mluAct = Doc hLocal Activity
{ activityId = mluAct
, activityActor = encodeRouteLocal $ SharerR shrUser
, activitySummary = Just summary
, activityAudience = audience
, activitySpecific = UndoActivity undo
}
now <- liftIO getCurrentTime
obiid <- insert OutboxItem
{ outboxItemOutbox = obid
, outboxItemActivity =
persistJSONObjectFromDoc $ activity Nothing
, outboxItemPublished = now
}
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
doc = activity $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (obiid, doc, luAct)
pushCommitsC
:: (Entity Person, Sharer)
-> Html
-> Push URIMode
-> ShrIdent
-> RpIdent
-> Handler (Either Text OutboxItemId)
pushCommitsC (eperson, sharer) summary push shrRepo rpRepo = runExceptT $ do
let dont = Authority "dont-do.any-forwarding" Nothing
(obiid, doc, remotesHttp) <- runDBExcept $ do
(obiid, doc) <- lift $ insertToOutbox
remoteRecips <- lift $ deliverLocal obiid
federation <- getsYesod $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled but remote collection members found"
remotesHttp <- lift $ deliverRemoteDB' dont obiid [] remoteRecips
return (obiid, doc, remotesHttp)
lift $ forkWorker "pushCommitsC: async HTTP delivery" $ deliverRemoteHttp dont obiid doc remotesHttp
return obiid
where
insertToOutbox :: AppDB (OutboxItemId, Doc Activity URIMode)
insertToOutbox = do
host <- getsYesod siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let shrUser = sharerIdent sharer
aud = map encodeRouteHome
[ SharerFollowersR shrUser
, RepoR shrRepo rpRepo
, RepoTeamR shrRepo rpRepo
, RepoFollowersR shrRepo rpRepo
]
activity mluAct = Doc host Activity
{ activityId = mluAct
, activityActor = encodeRouteLocal $ SharerR shrUser
, activitySummary =
Just $ TextHtml $ TL.toStrict $ renderHtml summary
, activityAudience = Audience aud [] [] [] [] []
, activitySpecific = PushActivity push
}
now <- liftIO getCurrentTime
obiid <- insert OutboxItem
{ outboxItemOutbox = personOutbox $ entityVal eperson
, outboxItemActivity = persistJSONObjectFromDoc $ activity Nothing
, outboxItemPublished = now
}
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
doc = activity $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (obiid, doc)
deliverLocal
:: OutboxItemId
-> AppDB
[ ( (InstanceId, Host)
, NonEmpty RemoteRecipient
)
]
deliverLocal obiid = do
let pidAuthor = entityKey eperson
(sidRepo, repo) <- do
sid <- getKeyBy404 $ UniqueSharer shrRepo
r <- getValBy404 $ UniqueRepo rpRepo sid
return (sid, r)
(pids, remotes) <- do
(repoPids, repoRemotes) <- getRepoTeam sidRepo
(pfsPids, pfsRemotes) <-
getFollowers $ personFollowers $ entityVal eperson
(rfsPids, rfsRemotes) <- getFollowers $ repoFollowers repo
return
( L.delete pidAuthor $ union repoPids $ union pfsPids rfsPids
, repoRemotes `unionRemotes` pfsRemotes `unionRemotes` rfsRemotes
)
ibiid <- insert $ InboxItem False
insert_ $ InboxItemLocal (repoInbox repo) obiid ibiid
for_ pids $ \ pid -> do
ibid <- personInbox <$> getJust pid
ibiid <- insert $ InboxItem True
insert_ $ InboxItemLocal ibid obiid ibiid
return remotes
getFollowersCollection
:: Route App -> AppDB FollowerSetId -> Handler TypedContent
getFollowersCollection here getFsid = do
(locals, remotes, l, r) <- runDB $ do
fsid <- getFsid
(,,,) <$> do pids <-
map (followPerson . entityVal) <$>
selectList
[FollowTarget ==. fsid, FollowPublic ==. True]
[]
sids <-
map (personIdent . entityVal) <$>
selectList [PersonId <-. pids] []
map (sharerIdent . entityVal) <$>
selectList [SharerId <-. sids] []
<*> do 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.&&. rf E.^. RemoteFollowPublic E.==. E.val True
return
( i E.^. InstanceHost
, ro E.^. RemoteObjectIdent
)
<*> count [FollowTarget ==. fsid]
<*> count [RemoteFollowTarget ==. fsid]
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let followersAP = Collection
{ collectionId = encodeRouteLocal here
, collectionType = CollectionTypeUnordered
, collectionTotalItems = Just $ l + r
, collectionCurrent = Nothing
, collectionFirst = Nothing
, collectionLast = Nothing
, collectionItems =
map (encodeRouteHome . SharerR) locals ++
map (uncurry ObjURI . bimap E.unValue E.unValue) remotes
}
provideHtmlAndAP followersAP $ redirectToPrettyJSON here