1
0
Fork 0
Vervis/src/Vervis/Federation/Ticket.hs
fr33domlover a2468c52fd Prepare for ticket dependency federation
To be honest, this is a huge patch that changes tons of stuff and probably
should have been broken up into small changes. But I already had the codebase
not building, so... just did all of this at once :P

Basically this patch does the following:

- DB migrations for ticket dependency related tables, e.g. allowing a remote
  author and a remote child
- Allowing S2S handlers to provide an async continued processing function,
  which is executed and the result then added to the debug page
- Most UI and functionality related to ticket deps is disabled, new
  implementation being added gradually via ActivityPub
- Improvements to AP tools, e.g. allow to specify multiple hosts for approved
  forwarding when sending out an activity, and allow to specify audience of
  software-authored activities using a convenient human-friendly structure
- Implementation of S2S sharerOfferDepF which creates a dependency under a
  sharer-hosted ticket/patch and sends back an Accept
2020-06-18 10:38:04 +00:00

1191 lines
55 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.Federation.Ticket
( sharerOfferTicketF
, projectOfferTicketF
, sharerCreateTicketF
, projectCreateTicketF
, sharerOfferDepF
)
where
import Control.Exception hiding (Handler)
import Control.Monad
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Data.Aeson
import Data.Bifunctor
import Data.Bitraversable
import Data.Foldable
import Data.Function
import Data.List (nub, union)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe
import Data.Text (Text)
import Data.Time.Calendar
import Data.Time.Clock
import Data.Traversable
import Database.Persist
import Text.Blaze.Html (preEscapedToHtml)
import Text.Blaze.Html.Renderer.Text
import Yesod.Core hiding (logError, logWarn, logInfo, logDebug)
import Yesod.Core.Handler
import Yesod.Persist.Core
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 Database.Persist.JSON
import Network.FedURI
import Web.ActivityPub hiding (Ticket (..))
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite
import qualified Web.ActivityPub as AP
import Control.Monad.Trans.Except.Local
import Data.Tuple.Local
import Database.Persist.Local
import Yesod.Persist.Local
import Vervis.ActivityPub
import Vervis.ActivityPub.Recipient
import Vervis.FedURI
import Vervis.Federation.Auth
import Vervis.Federation.Util
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Model.Ticket
import Vervis.Patch
import Vervis.Ticket
checkOffer
:: AP.Ticket URIMode
-> Host
-> ShrIdent
-> PrjIdent
-> ExceptT Text Handler ()
checkOffer ticket 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"
verifyNothingE (AP.ticketAttachment ticket) "Ticket with 'attachment'"
sharerOfferTicketF
:: UTCTime
-> ShrIdent
-> RemoteAuthor
-> ActivityBody
-> AP.Ticket URIMode
-> FedURI
-> ExceptT Text Handler Text
sharerOfferTicketF now shrRecip author body ticket uTarget = do
(hProject, shrProject, prjProject) <- parseTarget uTarget
luOffer <- fromMaybeE (activityId $ actbActivity body) "Offer without 'id'"
{-deps <- -}
checkOffer ticket hProject shrProject prjProject
local <- hostIsLocal hProject
runDBExcept $ do
ibidRecip <- lift $ do
sid <- getKeyBy404 $ UniqueSharer shrRecip
p <- getValBy404 $ UniquePersonIdent sid
return $ personInbox p
when local $ checkTargetAndDeps shrProject prjProject {-deps-}
lift $ insertToInbox luOffer ibidRecip
where
checkTargetAndDeps shrProject prjProject {-deps-} = do
msid <- lift $ getKeyBy $ UniqueSharer shrProject
sid <- fromMaybeE msid "Offer target: no such local sharer"
mjid <- lift $ getKeyBy $ UniqueProject prjProject sid
jid <- fromMaybeE mjid "Offer target: no such local project"
return ()
{-
for_ deps $ \ dep -> do
mt <- lift $ getBy $ UniqueTicket jid dep
unless (isJust mt) $
throwE "Local dep: No such ticket number in DB"
-}
insertToInbox luOffer ibidRecip = do
let iidAuthor = remoteAuthorInstance author
roid <-
either entityKey id <$> insertBy' (RemoteObject iidAuthor luOffer)
let jsonObj = persistJSONFromBL $ actbBL body
ract = RemoteActivity roid jsonObj now
ractid <- either entityKey id <$> insertBy' ract
ibiid <- insert $ InboxItem True
mibrid <- insertUnique $ InboxItemRemote ibidRecip ractid ibiid
let recip = shr2text shrRecip
case mibrid of
Nothing -> do
delete ibiid
return $ "Activity already exists in inbox of /s/" <> recip
Just _ -> return $ "Activity inserted to inbox of /s/" <> recip
data OfferTicketRecipColl
= OfferTicketRecipProjectFollowers
| OfferTicketRecipProjectTeam
deriving Eq
findRelevantCollections shrRecip prjRecip hLocal = nub . mapMaybe decide . concatRecipients
where
decide u = do
let ObjURI h lu = u
guard $ h == hLocal
route <- decodeRouteLocal lu
case route of
ProjectTeamR shr prj
| shr == shrRecip && prj == prjRecip
-> Just OfferTicketRecipProjectTeam
ProjectFollowersR shr prj
| shr == shrRecip && prj == prjRecip
-> Just OfferTicketRecipProjectFollowers
_ -> Nothing
-- | Perform inbox forwarding, delivering a remote activity we received to
-- local inboxes
deliverFwdLocal
:: RemoteActivityId
-> [OfferTicketRecipColl]
-> SharerId
-> FollowerSetId
-> AppDB [((InstanceId, Host), NonEmpty RemoteRecipient)]
deliverFwdLocal ractid recips sid fsid = do
(teamPids, teamRemotes) <-
if OfferTicketRecipProjectTeam `elem` recips
then getTicketTeam sid
else return ([], [])
(fsPids, fsRemotes) <-
if OfferTicketRecipProjectFollowers `elem` recips
then getFollowers fsid
else return ([], [])
let pids = union teamPids fsPids
remotes = unionRemotes teamRemotes fsRemotes
for_ pids $ \ pid -> do
ibid <- personInbox <$> getJust pid
ibiid <- insert $ InboxItem True
mibrid <- insertUnique $ InboxItemRemote ibid ractid ibiid
when (isNothing mibrid) $
delete ibiid
return remotes
projectOfferTicketF
:: UTCTime
-> ShrIdent
-> PrjIdent
-> RemoteAuthor
-> ActivityBody
-> AP.Ticket URIMode
-> FedURI
-> ExceptT Text Handler Text
projectOfferTicketF
now shrRecip prjRecip author body ticket uTarget = do
targetIsUs <- lift $ runExceptT checkTarget
case targetIsUs of
Left t -> do
logWarn $ T.concat
[ recip, " got Offer Ticket with target "
, renderObjURI uTarget
]
return t
Right () -> do
luOffer <-
fromMaybeE
(activityId $ actbActivity body)
"Offer without 'id'"
hLocal <- getsYesod siteInstanceHost
{-deps <- -}
checkOffer ticket hLocal shrRecip prjRecip
msig <- checkForward $ LocalActorProject shrRecip prjRecip
let colls =
findRelevantCollections shrRecip prjRecip hLocal $
activityAudience $ actbActivity body
mremotesHttp <- runDBExcept $ do
(sid, jid, ibid, fsid{-, tids-}) <-
getProjectAndDeps shrRecip prjRecip {-deps-}
lift $ do
mticket <- do
ra <- getJust $ remoteAuthorId author
insertTicket ra luOffer jid ibid {-tids-}
for mticket $ \ (ractid, obiidAccept, docAccept) -> do
msr <- for msig $ \ sig -> do
remoteRecips <- deliverFwdLocal ractid colls sid fsid
(sig,) <$> deliverRemoteDB_J (actbBL body) ractid jid sig remoteRecips
return (msr, obiidAccept, docAccept)
lift $ for_ mremotesHttp $ \ (msr, obiidAccept, docAccept) -> do
let handler e = logError $ "Project Accept sender: delivery failed! " <> T.pack (displayException e)
for msr $ \ (sig, remotesHttp) -> do
forkHandler handler $
deliverRemoteHTTP_J now shrRecip prjRecip (actbBL body) sig remotesHttp
forkHandler handler $ publishAccept luOffer obiidAccept docAccept
return $ recip <> " inserted new ticket"
where
recip = T.concat ["/s/", shr2text shrRecip, "/p/", prj2text prjRecip]
checkTarget = do
let ObjURI h lu = uTarget
local <- hostIsLocal h
unless local $
throwE $ recip <> " not using; target has different host"
route <-
case decodeRouteLocal lu of
Nothing ->
throwE $
recip <> " not using; local target isn't a valid route"
Just r -> return r
(shrTarget, prjTarget) <-
case route of
ProjectR shr prj -> return (shr, prj)
_ -> throwE $
recip <>
" not using; local target isn't a project route"
unless (shrTarget == shrRecip && prjTarget == prjRecip) $
throwE $ recip <> " not using; local target is a different project"
insertTicket ra luOffer jid ibid {-deps-} = do
let iidAuthor = remoteAuthorInstance author
roid <-
either entityKey id <$> insertBy' (RemoteObject iidAuthor luOffer)
let raidAuthor = remoteAuthorId author
ractid <- either entityKey id <$> insertBy' RemoteActivity
{ remoteActivityIdent = roid
, remoteActivityContent = persistJSONFromBL $ actbBL body
, remoteActivityReceived = now
}
ibiid <- insert $ InboxItem False
mibirid <- insertUnique $ InboxItemRemote ibid ractid ibiid
case mibirid of
Nothing -> do
delete ibiid
return Nothing
Just _ibirid -> do
{-
next <-
((subtract 1) . projectNextTicket) <$>
updateGet jid [ProjectNextTicket +=. 1]
-}
did <- insert Discussion
fsid <- insert FollowerSet
obiidAccept <- do
obidProject <- do
sid <- fromJust <$> getKeyBy (UniqueSharer shrRecip)
j <- fromJust <$> getValBy (UniqueProject prjRecip sid)
return $ projectOutbox j
hLocal <- asksSite siteInstanceHost
now <- liftIO getCurrentTime
insert OutboxItem
{ outboxItemOutbox = obidProject
, outboxItemActivity = persistJSONObjectFromDoc $ Doc hLocal emptyActivity
, outboxItemPublished = now
}
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
}
tclid <- insert TicketContextLocal
{ ticketContextLocalTicket = tid
, ticketContextLocalAccept = obiidAccept
}
insert_ TicketProjectLocal
{ ticketProjectLocalContext = tclid
, ticketProjectLocalProject = jid
}
insert_ TicketAuthorRemote
{ ticketAuthorRemoteTicket = tclid
, ticketAuthorRemoteAuthor = raidAuthor
, ticketAuthorRemoteOpen = ractid
}
docAccept <- insertAccept ra luOffer ltid obiidAccept
-- insertMany_ $ map (TicketDependency tid) deps
--insert_ $ RemoteFollow raidAuthor fsid False True
return $ Just (ractid, obiidAccept, docAccept)
insertAccept ra luOffer ltid obiid = do
let uAuthor@(ObjURI hAuthor luAuthor) = remoteAuthorURI author
ltkhid <- encodeKeyHashid ltid
summary <-
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
<a href="#{renderObjURI uAuthor}">
$maybe name <- remoteActorName ra
#{name}
$nothing
#{renderAuthority hAuthor}#{localUriPath luAuthor}
\'s ticket accepted by project #
<a href=@{ProjectR shrRecip prjRecip}>
./s/#{shr2text shrRecip}/p/#{prj2text prjRecip}
\: #
<a href=@{ProjectTicketR shrRecip prjRecip ltkhid}>
#{preEscapedToHtml $ unTextHtml $ AP.ticketSummary ticket}.
|]
hLocal <- asksSite siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
obikhid <- encodeKeyHashid obiid
let recips =
remoteAuthorURI author :
map encodeRouteHome
[ ProjectTeamR shrRecip prjRecip
, ProjectFollowersR shrRecip prjRecip
]
doc = Doc hLocal Activity
{ activityId =
Just $ encodeRouteLocal $
ProjectOutboxItemR shrRecip prjRecip obikhid
, activityActor =
encodeRouteLocal $ ProjectR shrRecip prjRecip
, activitySummary = Just summary
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject =
ObjURI
(objUriAuthority $ remoteAuthorURI author)
luOffer
, acceptResult =
Just $ encodeRouteLocal $
ProjectTicketR shrRecip prjRecip ltkhid
}
}
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return doc
publishAccept luOffer obiid doc = do
now <- liftIO getCurrentTime
let dont = Authority "dont-do.any-forwarding" Nothing
remotesHttp <- runDB $ do
(sid, project) <- do
sid <- fromJust <$> getKeyBy (UniqueSharer shrRecip)
j <- fromJust <$> getValBy (UniqueProject prjRecip sid)
return (sid, j)
moreRemotes <- deliverLocal now sid (projectFollowers project) obiid
let raidAuthor = remoteAuthorId author
ra <- getJust raidAuthor
ro <- getJust $ remoteActorIdent ra
let raInfo = RemoteRecipient raidAuthor (remoteObjectIdent ro) (remoteActorInbox ra) (remoteActorErrorSince ra)
iidAuthor = remoteAuthorInstance author
hAuthor = objUriAuthority $ remoteAuthorURI author
hostSection = ((iidAuthor, hAuthor), raInfo :| [])
remotes = unionRemotes [hostSection] moreRemotes
deliverRemoteDB' dont obiid [] remotes
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.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
checkCreateTicket
:: RemoteAuthor
-> AP.Ticket URIMode
-> Maybe FedURI
-> ExceptT
Text
Handler
( (Either (Bool, ShrIdent, PrjIdent) (Host, Maybe LocalURI, LocalURI))
, TicketLocal
, UTCTime
)
checkCreateTicket author ticket muTarget = do
mtarget <- traverse (checkTracker "Create target") muTarget
(context, ticketData, published) <- checkTicket ticket
(, ticketData, published) <$> checkTargetAndContext mtarget context
where
checkTracker name u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <-
fromMaybeE
(decodeRouteLocal lu)
(name <> " is local but isn't a valid route")
case route of
ProjectR shr prj -> return (shr, prj)
_ ->
throwE $
name <>
" is a valid local route, but isn't a project \
\route"
else return $ Right u
checkTicket (AP.Ticket mlocal attrib mpublished mupdated muContext _summary
_content _source muAssigned resolved mmr) = do
(hTicket, tlocal) <- fromMaybeE mlocal "Ticket without 'id'"
hl <- hostIsLocal hTicket
when hl $ throwE "Remote author claims to create local ticket"
unless (hTicket == objUriAuthority (remoteAuthorURI author)) $
throwE "Author created ticket hosted elsewhere"
unless (attrib == objUriLocal (remoteAuthorURI author)) $
throwE "Author created ticket attibuted to someone else"
uContext <- fromMaybeE muContext "Ticket without 'context'"
context <- checkTracker "Ticket context" uContext
pub <- fromMaybeE mpublished "Ticket without 'published'"
verifyNothingE mupdated "Ticket has 'updated'"
verifyNothingE muAssigned "Ticket has 'assignedTo'"
when resolved $ throwE "Ticket is resolved"
verifyNothingE mmr "Ticket has 'attachment'"
return (context, tlocal, pub)
checkTargetAndContext Nothing context =
return $
case context of
Left (shr, prj) -> Left (False, shr, prj)
Right (ObjURI h lu) -> Right (h, Nothing, lu)
checkTargetAndContext (Just target) context =
case (target, context) of
(Left _, Right _) ->
throwE "Create target is local but ticket context is remote"
(Right _, Left _) ->
throwE "Create target is remote but ticket context is local"
(Right (ObjURI hTarget luTarget), Right (ObjURI hContext luContext)) ->
if hTarget == hContext
then return $ Right (hTarget, Just luTarget, luContext)
else throwE "Create target and ticket context on \
\different remote hosts"
(Left (shr, prj), Left (shr', prj')) ->
if shr == shr' && prj == prj'
then return $ Left (True, shr, prj)
else throwE "Create target and ticket context are \
\different local projects"
sharerCreateTicketF
:: UTCTime
-> ShrIdent
-> RemoteAuthor
-> ActivityBody
-> AP.Ticket URIMode
-> Maybe FedURI
-> ExceptT Text Handler Text
sharerCreateTicketF now shrRecip author body ticket muTarget = do
luCreate <-
fromMaybeE (activityId $ actbActivity body) "Create without 'id'"
(targetAndContext, _, _) <- checkCreateTicket author ticket muTarget
runDBExcept $ do
ibidRecip <- lift $ do
sid <- getKeyBy404 $ UniqueSharer shrRecip
p <- getValBy404 $ UniquePersonIdent sid
return $ personInbox p
checkTargetAndContextDB targetAndContext
lift $ insertToInbox luCreate ibidRecip
where
checkTargetAndContextDB (Left (_, shr, prj)) = do
mj <- lift $ runMaybeT $ do
sid <- MaybeT $ getKeyBy $ UniqueSharer shr
MaybeT $ getBy $ UniqueProject prj sid
unless (isJust mj) $ throwE "Local context: No such project"
checkTargetAndContextDB (Right _) = return ()
insertToInbox luAct ibidRecip = do
let iidAuthor = remoteAuthorInstance author
roid <-
either entityKey id <$> insertBy' (RemoteObject iidAuthor luAct)
let jsonObj = persistJSONFromBL $ actbBL body
ract = RemoteActivity roid jsonObj now
ractid <- either entityKey id <$> insertBy' ract
ibiid <- insert $ InboxItem True
mibrid <- insertUnique $ InboxItemRemote ibidRecip ractid ibiid
let recip = shr2text shrRecip
case mibrid of
Nothing -> do
delete ibiid
return $ "Activity already exists in inbox of /s/" <> recip
Just _ -> return $ "Activity inserted to inbox of /s/" <> recip
projectCreateTicketF
:: UTCTime
-> ShrIdent
-> PrjIdent
-> RemoteAuthor
-> ActivityBody
-> AP.Ticket URIMode
-> Maybe FedURI
-> ExceptT Text Handler Text
projectCreateTicketF now shrRecip prjRecip author body ticket muTarget = do
luCreate <-
fromMaybeE (activityId $ actbActivity body) "Create without 'id'"
(targetAndContext, tlocal, published) <- checkCreateTicket author ticket muTarget
case targetAndContext of
Left (_, shrContext, prjContext)
| shrRecip == shrContext && prjRecip == prjContext -> do
msig <- checkForward $ LocalActorProject shrRecip prjRecip
msgOrRecips <- lift $ runDB $ do
(sidProject, jid, obidProject, ibidProject, fsidProject) <- getProject
mractidCreate <- insertCreate luCreate ibidProject
case mractidCreate of
Nothing -> return $ Left "Already have this activity in project inbox, ignoring"
Just ractidCreate -> do
(obiidAccept, docAccept, localRecipsAccept, remoteRecipsAccept, fwdAccept) <- insertAccept obidProject luCreate tlocal
result <- insertTicket jid (AP.ticketId tlocal) published ractidCreate obiidAccept
case result of
Left False -> do
delete obiidAccept
return $ Left "Already have a ticket opened by this activity, ignoring"
Left True -> do
delete obiidAccept
return $ Left "Already have this ticket, ignoring"
Right () -> do
hLocal <- getsYesod siteInstanceHost
let colls = findRelevantCollections shrRecip prjRecip hLocal $ activityAudience $ actbActivity body
mremoteRecipsHttpCreateFwd <- for msig $ \ sig -> do
remoteRecips <- deliverFwdLocal ractidCreate colls sidProject fsidProject
(sig,) <$> deliverRemoteDB_J (actbBL body) ractidCreate jid sig remoteRecips
remoteRecipsHttpAccept <- do
moreRemoteRecipsAccept <- deliverLocal' False (LocalActorProject shrRecip prjRecip) ibidProject obiidAccept localRecipsAccept
deliverRemoteDB' fwdAccept obiidAccept remoteRecipsAccept moreRemoteRecipsAccept
return $ Right (mremoteRecipsHttpCreateFwd, remoteRecipsHttpAccept, obiidAccept, docAccept, fwdAccept)
case msgOrRecips of
Left msg -> return msg
Right (mremoteRecipsHttpCreateFwd, remoteRecipsHttpAccept, obiidAccept, docAccept, fwdAccept) -> do
for_ mremoteRecipsHttpCreateFwd $ \ (sig, recips) -> forkWorker "projectCreateTicketF inbox forwarding" $ deliverRemoteHTTP_J now shrRecip prjRecip (actbBL body) sig recips
forkWorker "projectCreateTicketF deliver Accept" $ deliverRemoteHttp fwdAccept obiidAccept docAccept remoteRecipsHttpAccept
return "Accepting and listing new remote author hosted ticket"
_ -> return "Create/Ticket against different project, ignoring"
where
getProject = do
sid <- getKeyBy404 $ UniqueSharer shrRecip
Entity jid j <- getBy404 $ UniqueProject prjRecip sid
return (sid, jid, projectOutbox j, projectInbox j, projectFollowers j)
insertCreate luCreate ibidProject = do
roid <- either entityKey id <$> insertBy' RemoteObject
{ remoteObjectInstance = remoteAuthorInstance author
, remoteObjectIdent = luCreate
}
let raidAuthor = remoteAuthorId author
ractidCreate <- either entityKey id <$> insertBy' RemoteActivity
{ remoteActivityIdent = roid
, remoteActivityContent = persistJSONFromBL $ actbBL body
, remoteActivityReceived = now
}
ibiid <- insert $ InboxItem False
mibirid <-
insertUnique $ InboxItemRemote ibidProject ractidCreate ibiid
case mibirid of
Nothing -> do
delete ibiid
return Nothing
Just _ -> return $ Just ractidCreate
insertAccept obidProject luCreate tlocal = do
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
hLocal <- asksSite siteInstanceHost
obiidAccept <- insert OutboxItem
{ outboxItemOutbox = obidProject
, outboxItemActivity =
persistJSONObjectFromDoc $ Doc hLocal emptyActivity
, outboxItemPublished = now
}
obikhidAccept <- encodeKeyHashid obiidAccept
ra <- getJust $ remoteAuthorId author
summary <- do
let uAuthor@(ObjURI hAuthor luAuthor) = remoteAuthorURI author
TextHtml . TL.toStrict . renderHtml <$>
withUrlRenderer
[hamlet|
<p>
<a href="#{renderObjURI uAuthor}">
$maybe name <- remoteActorName ra
#{name}
$nothing
#{renderAuthority hAuthor}#{localUriPath luAuthor}
\'s ticket accepted and listed by project #
<a href=@{ProjectR shrRecip prjRecip}>
./s/#{shr2text shrRecip}/p/#{prj2text prjRecip}
\: #
<a href="#{renderObjURI $ ObjURI hAuthor $ AP.ticketId tlocal}">
#{preEscapedToHtml $ unTextHtml $ AP.ticketSummary ticket}.
|]
let localRecipsA =
[
]
localRecipsC =
[ LocalPersonCollectionProjectTeam shrRecip prjRecip
, LocalPersonCollectionProjectFollowers shrRecip prjRecip
]
remoteRecipsA =
objUriLocal (remoteAuthorURI author) :| []
remoteRecipsC = catMaybes
[ remoteActorFollowers ra
, Just $ AP.ticketParticipants tlocal
, AP.ticketTeam tlocal
]
localRecips =
map encodeRouteHome $
map renderLocalActor localRecipsA ++
map renderLocalPersonCollection localRecipsC
remoteRecips =
map (ObjURI $ objUriAuthority $ remoteAuthorURI author) $
NE.toList remoteRecipsA ++ remoteRecipsC
recips = localRecips ++ remoteRecips
doc = Doc hLocal Activity
{ activityId =
Just $ encodeRouteLocal $
ProjectOutboxItemR shrRecip prjRecip obikhidAccept
, activityActor =
encodeRouteLocal $ ProjectR shrRecip prjRecip
, activitySummary = Just summary
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject =
ObjURI
(objUriAuthority $ remoteAuthorURI author)
luCreate
, acceptResult = Nothing
}
}
update obiidAccept [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return
( obiidAccept
, doc
, makeRecipientSet localRecipsA localRecipsC
, [(objUriAuthority $ remoteAuthorURI author, remoteRecipsA)]
, objUriAuthority $ remoteAuthorURI author
)
insertTicket jid luTicket published ractidCreate obiidAccept = do
tid <- insert Ticket
{ ticketNumber = Nothing
, ticketCreated = published
, 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
}
tclid <- insert TicketContextLocal
{ ticketContextLocalTicket = tid
, ticketContextLocalAccept = obiidAccept
}
tplid <- insert TicketProjectLocal
{ ticketProjectLocalContext = tclid
, ticketProjectLocalProject = jid
}
mtarid <- insertUnique TicketAuthorRemote
{ ticketAuthorRemoteTicket = tclid
, ticketAuthorRemoteAuthor = remoteAuthorId author
, ticketAuthorRemoteOpen = ractidCreate
}
case mtarid of
Nothing -> do
delete tplid
delete tclid
delete tid
return $ Left False
Just tarid -> do
roid <- either entityKey id <$> insertBy' RemoteObject
{ remoteObjectInstance = remoteAuthorInstance author
, remoteObjectIdent = luTicket
}
did <- insert Discussion
(rdid, rdnew) <- idAndNew <$> insertBy' RemoteDiscussion
{ remoteDiscussionIdent = roid
, remoteDiscussionDiscuss = did
}
unless rdnew $ delete did
mrtid <- insertUnique RemoteTicket
{ remoteTicketTicket = tarid
, remoteTicketIdent = roid
, remoteTicketDiscuss = rdid
}
case mrtid of
Nothing -> do
delete tarid
delete tplid
delete tclid
delete tid
return $ Left True
Just _rtid -> return $ Right ()
sharerOfferDepF
:: UTCTime
-> ShrIdent
-> RemoteAuthor
-> ActivityBody
-> AP.TicketDependency URIMode
-> FedURI
-> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
sharerOfferDepF now shrRecip author body dep uTarget = do
luOffer <- fromMaybeE (activityId $ actbActivity body) "Offer without 'id'"
(parent, child) <- checkDepAndTarget dep uTarget
(localRecips, _remoteRecips) <- do
mrecips <- parseAudience $ activityAudience $ actbActivity body
fromMaybeE mrecips "Offer Dep with no recipients"
msig <- checkForward $ LocalActorSharer shrRecip
personRecip <- lift $ runDB $ do
sid <- getKeyBy404 $ UniqueSharer shrRecip
getValBy404 $ UniquePersonIdent sid
return $ (,) "Ran initial checks, doing the rest asynchronously" $ Just $ do
manager <- asksSite appHttpManager
relevantParent <-
for (parentRelevance shrRecip parent) $ \ (talid, patch) -> do
(parentLtid, parentCtx) <- runSiteDBExcept $ do
let getTcr tcr = do
let getRoid roid = do
ro <- getJust roid
i <- getJust $ remoteObjectInstance ro
return $ mkuri (i, ro)
roidT <- remoteActorIdent <$> getJust (ticketProjectRemoteTracker tcr)
let mroidJ = ticketProjectRemoteProject tcr
(,) <$> getRoid roidT <*> traverse getRoid mroidJ
if patch
then do
(_, Entity ltid _, _, context, _) <- do
mticket <- lift $ getSharerPatch shrRecip talid
fromMaybeE mticket $ "Parent" <> ": No such sharer-patch"
context' <-
lift $
bitraverse
(\ (_, Entity _ trl) -> do
r <- getJust $ ticketRepoLocalRepo trl
s <- getJust $ repoSharer r
return $ Right (sharerIdent s, repoIdent r)
)
(\ (Entity _ tcr, _) -> getTcr tcr)
context
return (ltid, context')
else do
(_, Entity ltid _, _, context) <- do
mticket <- lift $ getSharerTicket shrRecip talid
fromMaybeE mticket $ "Parent" <> ": No such sharer-ticket"
context' <-
lift $
bitraverse
(\ (_, Entity _ tpl) -> do
j <- getJust $ ticketProjectLocalProject tpl
s <- getJust $ projectSharer j
return $ Left (sharerIdent s, projectIdent j)
)
(\ (Entity _ tcr, _) -> getTcr tcr)
context
return (ltid, context')
parentCtx' <- bifor parentCtx pure $ \ (uTracker, muProject) -> do
let uProject = fromMaybe uTracker muProject
obj <- withExceptT T.pack $ AP.fetchAP manager $ Left uProject
unless (objId obj == uProject) $
throwE "Project 'id' differs from the URI we fetched"
return
(uTracker, objUriAuthority uProject, objFollowers obj, objTeam obj)
(childId, childCtx, childAuthor) <-
case child of
Left wi -> runSiteDBExcept $ do
(ltid, ctx, author) <- getWorkItem "Child" wi
return (Left (wi, ltid), second mkuri ctx, second mkuri author)
Right u -> do
Doc hAuthor t <- withExceptT T.pack $ AP.fetchAP manager $ Left u
(hTicket, tl) <- fromMaybeE (AP.ticketLocal t) "Child ticket no 'id'"
unless (ObjURI hAuthor (AP.ticketId tl) == u) $
throwE "Ticket 'id' differs from the URI we fetched"
uCtx <- fromMaybeE (AP.ticketContext t) "Ticket without 'context'"
ctx <- parseTicketContext uCtx
author <- parseTicketAuthor $ ObjURI hTicket (AP.ticketAttributedTo t)
return (Right (u, AP.ticketParticipants tl), ctx, author)
childCtx' <- bifor childCtx pure $ \ u -> do
obj <- withExceptT T.pack $ AP.fetchAP manager $ Left u
unless (objId obj == u) $
throwE "Project 'id' differs from the URI we fetched"
u' <-
case (objContext obj, objInbox obj) of
(Just c, Nothing) -> do
hl <- hostIsLocal $ objUriAuthority c
when hl $ throwE "Child remote context has a local context"
pure c
(Nothing, Just _) -> pure u
_ -> throwE "Umm context-inbox thing"
return
(u', objUriAuthority u, objFollowers obj, objTeam obj)
return (talid, patch, parentLtid, parentCtx', childId, childCtx', childAuthor)
mhttp <- lift $ runSiteDB $ do
mractid <- insertToInbox now author body (personInbox personRecip) luOffer True
for mractid $ \ ractid -> do
mremotesHttpFwd <- for msig $ \ sig -> do
relevantFollowers <- askRelevantFollowers
let sieve =
makeRecipientSet [] $ catMaybes
[ relevantFollowers shrRecip parent
, relevantFollowers shrRecip child
]
remoteRecips <-
insertRemoteActivityToLocalInboxes
False ractid $
localRecipSieve'
sieve False False localRecips
(sig,) <$> deliverRemoteDB_S (actbBL body) ractid (personIdent personRecip) sig remoteRecips
mremotesHttpAccept <- for relevantParent $ \ ticketData@(_, _, parentLtid, _, childId, _, _) -> do
obiidAccept <- insertEmptyOutboxItem (personOutbox personRecip) now
tdid <- insertDep ractid parentLtid childId obiidAccept
(docAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
insertAccept luOffer obiidAccept tdid ticketData
knownRemoteRecipsAccept <-
deliverLocal'
False
(LocalActorSharer shrRecip)
(personInbox personRecip)
obiidAccept
localRecipsAccept
(obiidAccept,docAccept,fwdHostsAccept,) <$> deliverRemoteDB'' fwdHostsAccept obiidAccept remoteRecipsAccept knownRemoteRecipsAccept
return (mremotesHttpFwd, mremotesHttpAccept)
case mhttp of
Nothing -> return "I already have this activity in my inbox, doing nothing"
Just (mremotesHttpFwd, mremotesHttpAccept) -> do
for_ mremotesHttpFwd $ \ (sig, remotes) ->
forkWorker "sharerOfferDepF inbox-forwarding" $
deliverRemoteHTTP_S now shrRecip (actbBL body) sig remotes
for_ mremotesHttpAccept $ \ (obiid, doc, fwdHosts, remotes) ->
forkWorker "sharerOfferDepF Accept HTTP delivery" $
deliverRemoteHttp' fwdHosts obiid doc remotes
return $
case (mremotesHttpAccept, mremotesHttpFwd) of
(Nothing, Nothing) -> "Parent not mine, just stored in inbox and no inbox-forwarding to do"
(Nothing, Just _) -> "Parent not mine, just stored in inbox and ran inbox-forwarding"
(Just _, Nothing) -> "Accepted new ticket dep, no inbox-forwarding to do"
(Just _, Just _) -> "Accepted new ticket dep and ran inbox-forwarding of the Offer"
where
checkDepAndTarget
(AP.TicketDependency id_ uParent uChild _attrib published updated) uTarget = do
verifyNothingE id_ "Dep with 'id'"
parent <- parseWorkItem "Dep parent" uParent
child <- parseWorkItem "Dep child" uChild
when (parent == child) $
throwE "Parent and child are the same work item"
verifyNothingE published "Dep with 'published'"
verifyNothingE updated "Dep with 'updated'"
target <- parseTarget uTarget
checkParentAndTarget parent target
return (parent, child)
where
parseWorkItem name u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <-
fromMaybeE (decodeRouteLocal lu) $
name <> ": Not a valid route"
case route of
SharerTicketR shr talkhid -> do
talid <- decodeKeyHashidE talkhid $ name <> ": Invalid talkhid"
return $ WorkItemSharerTicket shr talid False
SharerPatchR shr talkhid -> do
talid <- decodeKeyHashidE talkhid $ name <> ": Invalid talkhid"
return $ WorkItemSharerTicket shr talid True
ProjectTicketR shr prj ltkhid -> do
ltid <- decodeKeyHashidE ltkhid $ name <> ": Invalid ltkhid"
return $ WorkItemProjectTicket shr prj ltid
RepoPatchR shr rp ltkhid -> do
ltid <- decodeKeyHashidE ltkhid $ name <> ": Invalid ltkhid"
return $ WorkItemRepoPatch shr rp ltid
_ -> throwE $ name <> ": not a work item route"
else return $ Right u
parseTarget u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <-
fromMaybeE
(decodeRouteLocal lu)
"Offer local target isn't a valid route"
fromMaybeE
(parseLocalActor route)
"Offer local target isn't an actor route"
else return $ Right u
checkParentAndTarget (Left wi) (Left la) =
unless (workItemActor wi == la) $
throwE "Parent and target mismatch"
where
workItemActor (WorkItemSharerTicket shr _ _) = LocalActorSharer shr
workItemActor (WorkItemProjectTicket shr prj _) = LocalActorProject shr prj
workItemActor (WorkItemRepoPatch shr rp _) = LocalActorRepo shr rp
checkParentAndTarget (Left _) (Right _) = throwE "Local parent but remote target"
checkParentAndTarget (Right _) (Left _) = throwE "Local target but remote parent"
checkParentAndTarget (Right _) (Right _) = return ()
parentRelevance shr (Left (WorkItemSharerTicket shr' talid patch))
| shr == shr' = Just (talid, patch)
parentRelevance _ _ = Nothing
{-
getWorkItem
:: MonadIO m
=> Text
-> WorkItem
-> ExceptT Text (ReaderT SqlBaclend m)
( LocalTicketId
, Either
(Either (ShrIdent, PrjIdent) (ShrIdent, RpIdent))
(Instance, RemoteObject)
, Either ShrIdent (Instance, RemoteObject)
)
-}
getWorkItem name (WorkItemSharerTicket shr talid False) = do
(_, Entity ltid _, _, context) <- do
mticket <- lift $ getSharerTicket shr talid
fromMaybeE mticket $ name <> ": No such sharer-ticket"
context' <-
lift $
bitraverse
(\ (_, Entity _ tpl) -> do
j <- getJust $ ticketProjectLocalProject tpl
s <- getJust $ projectSharer j
return $ Left (sharerIdent s, projectIdent j)
)
(\ (Entity _ tcr, _) -> do
roid <-
case ticketProjectRemoteProject tcr of
Nothing ->
remoteActorIdent <$>
getJust (ticketProjectRemoteTracker tcr)
Just roid -> return roid
ro <- getJust roid
i <- getJust $ remoteObjectInstance ro
return (i, ro)
)
context
return (ltid, context', Left shr)
getWorkItem name (WorkItemSharerTicket shr talid True) = do
(_, Entity ltid _, _, context, _) <- do
mticket <- lift $ getSharerPatch shr talid
fromMaybeE mticket $ name <> ": No such sharer-patch"
context' <-
lift $
bitraverse
(\ (_, Entity _ trl) -> do
r <- getJust $ ticketRepoLocalRepo trl
s <- getJust $ repoSharer r
return $ Right (sharerIdent s, repoIdent r)
)
(\ (Entity _ tcr, _) -> do
roid <-
case ticketProjectRemoteProject tcr of
Nothing ->
remoteActorIdent <$>
getJust (ticketProjectRemoteTracker tcr)
Just roid -> return roid
ro <- getJust roid
i <- getJust $ remoteObjectInstance ro
return (i, ro)
)
context
return (ltid, context', Left shr)
getWorkItem name (WorkItemProjectTicket shr prj ltid) = do
mticket <- lift $ getProjectTicket shr prj ltid
(Entity _ s, Entity _ j, _, _, _, _, author) <-
fromMaybeE mticket $ name <> ": No such project-ticket"
author' <-
lift $
bitraverse
(\ (Entity _ tal, _) -> do
p <- getJust $ ticketAuthorLocalAuthor tal
sharerIdent <$> getJust (personIdent p)
)
(\ (Entity _ tar) -> do
ra <- getJust $ ticketAuthorRemoteAuthor tar
ro <- getJust $ remoteActorIdent ra
i <- getJust $ remoteObjectInstance ro
return (i, ro)
)
author
return (ltid, Left $ Left (sharerIdent s, projectIdent j), author')
getWorkItem name (WorkItemRepoPatch shr rp ltid) = do
mticket <- lift $ getRepoPatch shr rp ltid
(Entity _ s, Entity _ r, _, _, _, _, author, _) <-
fromMaybeE mticket $ name <> ": No such repo-patch"
author' <-
lift $
bitraverse
(\ (Entity _ tal, _) -> do
p <- getJust $ ticketAuthorLocalAuthor tal
sharerIdent <$> getJust (personIdent p)
)
(\ (Entity _ tar) -> do
ra <- getJust $ ticketAuthorRemoteAuthor tar
ro <- getJust $ remoteActorIdent ra
i <- getJust $ remoteObjectInstance ro
return (i, ro)
)
author
return (ltid, Left $ Right (sharerIdent s, repoIdent r), author')
mkuri (i, ro) = ObjURI (instanceHost i) (remoteObjectIdent ro)
parseTicketContext u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <- fromMaybeE (decodeRouteLocal lu) "Not a route"
case route of
ProjectR shr prj -> return $ Left (shr, prj)
RepoR shr rp -> return $ Right (shr, rp)
_ -> throwE "Not a ticket context route"
else return $ Right u
parseTicketAuthor u@(ObjURI h lu) = do
hl <- hostIsLocal h
if hl
then Left <$> do
route <- fromMaybeE (decodeRouteLocal lu) "Not a route"
case route of
SharerR shr -> return shr
_ -> throwE "Not a ticket author route"
else return $ Right u
askRelevantFollowers = do
hashTALID <- getEncodeKeyHashid
return $ \ shr wi -> followers hashTALID <$> parentRelevance shr wi
where
followers hashTALID (talid, patch) =
let coll =
if patch
then LocalPersonCollectionSharerPatchFollowers
else LocalPersonCollectionSharerTicketFollowers
in coll shrRecip (hashTALID talid)
insertDep ractidOffer ltidParent child obiidAccept = do
tdid <- insert LocalTicketDependency
{ localTicketDependencyParent = ltidParent
, localTicketDependencyCreated = now
, localTicketDependencyAccept = obiidAccept
}
case child of
Left (_wi, ltid) -> insert_ TicketDependencyChildLocal
{ ticketDependencyChildLocalDep = tdid
, ticketDependencyChildLocalChild = ltid
}
Right (ObjURI h lu, _luFollowers) -> do
iid <- either entityKey id <$> insertBy' (Instance h)
roid <- either entityKey id <$> insertBy' (RemoteObject iid lu)
insert_ TicketDependencyChildRemote
{ ticketDependencyChildRemoteDep = tdid
, ticketDependencyChildRemoteChild = roid
}
insert_ TicketDependencyAuthorRemote
{ ticketDependencyAuthorRemoteDep = tdid
, ticketDependencyAuthorRemoteAuthor = remoteAuthorId author
, ticketDependencyAuthorRemoteOpen = ractidOffer
}
return tdid
insertAccept luOffer obiidAccept tdid (talid, patch, _, parentCtx, childId, childCtx, childAuthor) = do
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
followers <- askFollowers
workItemFollowers <- askWorkItemFollowers
hLocal <- asksSite siteInstanceHost
obikhidAccept <- encodeKeyHashid obiidAccept
tdkhid <- encodeKeyHashid tdid
ra <- getJust $ remoteAuthorId author
let ObjURI hAuthor luAuthor = remoteAuthorURI author
audAuthor =
AudRemote hAuthor [luAuthor] (maybeToList $ remoteActorFollowers ra)
audParentContext = contextAudience parentCtx
audChildContext = contextAudience childCtx
audParent = AudLocal [LocalActorSharer shrRecip] [followers talid patch]
audChildAuthor =
case childAuthor of
Left shr -> AudLocal [LocalActorSharer shr] []
Right (ObjURI h lu) -> AudRemote h [lu] []
audChildFollowers =
case childId of
Left (wi, _ltid) -> AudLocal [] [workItemFollowers wi]
Right (ObjURI h _, luFollowers) -> AudRemote h [] [luFollowers]
(recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
collectAudience $
audAuthor :
audParent :
audChildAuthor :
audChildFollowers :
audParentContext ++ audChildContext
recips = map encodeRouteHome audLocal ++ audRemote
doc = Doc hLocal Activity
{ activityId =
Just $ encodeRouteLocal $
SharerOutboxItemR shrRecip obikhidAccept
, activityActor = encodeRouteLocal $ SharerR shrRecip
, activitySummary = Nothing
, activityAudience = Audience recips [] [] [] [] []
, activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hAuthor luOffer
, acceptResult =
Just $ encodeRouteLocal $ TicketDepR tdkhid
}
}
update obiidAccept [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (doc, recipientSet, remoteActors, fwdHosts)
where
contextAudience ctx =
case ctx of
Left (Left (shr, prj)) ->
pure $ AudLocal
[LocalActorProject shr prj]
[ LocalPersonCollectionProjectTeam shr prj
, LocalPersonCollectionProjectFollowers shr prj
]
Left (Right (shr, rp)) ->
pure $ AudLocal
[LocalActorRepo shr rp]
[ LocalPersonCollectionRepoTeam shr rp
, LocalPersonCollectionRepoFollowers shr rp
]
Right (ObjURI hTracker luTracker, hProject, luFollowers, luTeam) ->
[ AudRemote hTracker [luTracker] []
, AudRemote hProject [] (catMaybes [luFollowers, luTeam])
]
askFollowers = do
hashTALID <- getEncodeKeyHashid
return $ \ talid patch ->
let coll =
if patch
then LocalPersonCollectionSharerPatchFollowers
else LocalPersonCollectionSharerTicketFollowers
in coll shrRecip (hashTALID talid)
askWorkItemFollowers = do
hashTALID <- getEncodeKeyHashid
hashLTID <- getEncodeKeyHashid
let workItemFollowers (WorkItemSharerTicket shr talid False) = LocalPersonCollectionSharerTicketFollowers shr $ hashTALID talid
workItemFollowers (WorkItemSharerTicket shr talid True) = LocalPersonCollectionSharerPatchFollowers shr $ hashTALID talid
workItemFollowers (WorkItemProjectTicket shr prj ltid) = LocalPersonCollectionProjectTicketFollowers shr prj $ hashLTID ltid
workItemFollowers (WorkItemRepoPatch shr rp ltid) = LocalPersonCollectionRepoPatchFollowers shr rp $ hashLTID ltid
return workItemFollowers