Handle post-receive hook, publish a Push activity

This commit is contained in:
fr33domlover 2019-09-09 00:27:45 +00:00
parent 3c01f4136c
commit 68e8b094a0
22 changed files with 545 additions and 73 deletions

View file

@ -36,12 +36,14 @@ Person
about Text
inbox InboxId
outbox OutboxId
followers FollowerSetId
UniquePersonIdent ident
UniquePersonLogin login
UniquePersonEmail email
UniquePersonInbox inbox
UniquePersonOutbox outbox
UniquePersonFollowers followers
Outbox
@ -235,8 +237,12 @@ Repo
mainBranch Text
collabUser RoleId Maybe
collabAnon RoleId Maybe
inbox InboxId
followers FollowerSetId
UniqueRepo ident sharer
UniqueRepoInbox inbox
UniqueRepoFollowers followers
Workflow
sharer SharerId

View file

@ -62,6 +62,7 @@
/s/#ShrIdent/notifications NotificationsR GET POST
/s/#ShrIdent/outbox SharerOutboxR GET POST
/s/#ShrIdent/outbox/#OutboxItemKeyHashid SharerOutboxItemR GET
/s/#ShrIdent/followers SharerFollowersR GET
/p PeopleR GET
@ -84,6 +85,9 @@
/s/#ShrIdent/r ReposR GET POST
/s/#ShrIdent/r/!new RepoNewR GET
/s/#ShrIdent/r/#RpIdent RepoR GET PUT DELETE POST
/s/#ShrIdent/r/#RpIdent/inbox RepoInboxR GET POST
/s/#ShrIdent/r/#RpIdent/team RepoTeamR GET
/s/#ShrIdent/r/#RpIdent/followers RepoFollowersR GET
/s/#ShrIdent/r/#RpIdent/edit RepoEditR GET
/s/#ShrIdent/r/#RpIdent/s/+Texts RepoSourceR GET
/s/#ShrIdent/r/#RpIdent/c RepoHeadChangesR GET

View file

@ -0,0 +1,10 @@
Inbox
FollowerSet
Repo
inbox InboxId
followers FollowerSetId
Person
followers FollowerSetId

View file

@ -16,6 +16,7 @@
module Vervis.API
( createNoteC
, offerTicketC
, pushCommitsC
, getFollowersCollection
)
where
@ -691,6 +692,87 @@ offerTicketC shrUser summary audience offer@(Offer ticket uTarget) = runExceptT
insert_ $ InboxItemLocal ibid obiid ibiid
return remotes
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
, 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 (RemoteActorId, LocalURI, LocalURI, Maybe UTCTime)
)
]
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
@ -725,4 +807,4 @@ getFollowersCollection here getFsid = do
map (encodeRouteHome . SharerR) locals ++
map (uncurry ObjURI . bimap E.unValue E.unValue) remotes
}
provideHtmlAndAP followersAP $ redirect (here, [("prettyjson", "true")])
provideHtmlAndAP followersAP $ redirectToPrettyJSON here

View file

@ -24,6 +24,7 @@ module Vervis.ActivityPub
, getPersonOrGroupId
, getTicketTeam
, getProjectTeam
, getRepoTeam
, getFollowers
, unionRemotes
, insertMany'
@ -211,6 +212,8 @@ getTicketTeam sid = do
getProjectTeam = getTicketTeam
getRepoTeam = getTicketTeam
getFollowers :: FollowerSetId -> AppDB ([PersonId], [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, Maybe UTCTime))])
getFollowers fsid = do
local <- selectList [FollowTarget ==. fsid] [Asc FollowPerson]

View file

@ -16,6 +16,7 @@
module Vervis.Federation
( handleSharerInbox
, handleProjectInbox
, handleRepoInbox
, fixRunningDeliveries
, retryOutboxDelivery
)
@ -253,6 +254,28 @@ handleProjectInbox now shrRecip prjRecip auth body = do
projectOfferTicketF now shrRecip prjRecip remoteAuthor body offer
_ -> return "Unsupported activity type"
handleRepoInbox
:: UTCTime
-> ShrIdent
-> RpIdent
-> ActivityAuthentication
-> ActivityBody
-> ExceptT Text Handler Text
handleRepoInbox now shrRecip rpRecip auth body = do
remoteAuthor <-
case auth of
ActivityAuthLocalPerson pid ->
throwE $
"Repo inbox got local forwarded activity by pid#" <>
T.pack (show $ fromSqlKey pid)
ActivityAuthLocalProject jid ->
throwE $
"Repo inbox got local forwarded activity by jid#" <>
T.pack (show $ fromSqlKey jid)
ActivityAuthRemote ra -> return ra
case activitySpecific $ actbActivity body of
_ -> return "Unsupported activity type"
fixRunningDeliveries :: (MonadIO m, MonadLogger m, IsSqlBackend backend) => ReaderT backend m ()
fixRunningDeliveries = do
c <- updateWhereCount [UnlinkedDeliveryRunning ==. True] [UnlinkedDeliveryRunning =. False]

View file

@ -320,13 +320,39 @@ verifyForwardedSig hAuthor luAuthor (Verification malgo keyid input signature) =
mkauth (Left pid) = ActivityAuthLocalPerson pid
mkauth (Right jid) = ActivityAuthLocalProject jid
verifyContentTypeAP :: MonadHandler m => m ()
verifyContentTypeAP = do
result <- runExceptT verifyContentTypeAP_E
case result of
Left e -> invalidArgs ["Content type error: " <> e]
Right () -> return ()
verifyContentTypeAP_E :: MonadHandler m => ExceptT Text m ()
verifyContentTypeAP_E = do
ctypes <- lookupHeaders "Content-Type"
case ctypes of
[] -> throwE "Content-Type not specified"
[x] | x == typeAS -> return ()
| x == typeAS2 -> return ()
| otherwise ->
throwE $ "Not a recognized AP Content-Type: " <>
case decodeUtf8' x of
Left _ -> T.pack (show x)
Right t -> t
_ -> throwE "More than one Content-Type specified"
where
typeAS = "application/activity+json"
typeAS2 =
"application/ld+json; \
\profile=\"https://www.w3.org/ns/activitystreams\""
authenticateActivity
:: UTCTime
-- -> ExceptT Text Handler (Either PersonId ActivityDetail, BL.ByteString, Object, Activity)
-> ExceptT Text Handler (ActivityAuthentication, ActivityBody)
authenticateActivity now = do
(ra, wv, body) <- do
verifyContentType
verifyContentTypeAP_E
proof <- withExceptT (T.pack . displayException) $ ExceptT $ do
timeLimit <- getsYesod $ appHttpSigTimeLimit . appSettings
let requires = [hRequestTarget, hHost, hDigest]
@ -371,23 +397,6 @@ authenticateActivity now = do
Just a -> return a
return (auth, ActivityBody body raw activity)
where
verifyContentType = do
ctypes <- lookupHeaders "Content-Type"
case ctypes of
[] -> throwE "Content-Type not specified"
[x] | x == typeAS -> return ()
| x == typeAS2 -> return ()
| otherwise ->
throwE $ "Not a recognized AP Content-Type: " <>
case decodeUtf8' x of
Left _ -> T.pack (show x)
Right t -> t
_ -> throwE "More than one Content-Type specified"
where
typeAS = "application/activity+json"
typeAS2 =
"application/ld+json; \
\profile=\"https://www.w3.org/ns/activitystreams\""
verifyBodyDigest = do
req <- waiRequest
let headers = W.requestHeaders req

View file

@ -99,6 +99,8 @@ editRepoAForm sid (Entity rid repo) = Repo
)
<*> aopt selectRole "User role" (Just $ repoCollabUser repo)
<*> aopt selectRole "Guest role" (Just $ repoCollabAnon repo)
<*> pure (repoInbox repo)
<*> pure (repoFollowers repo)
where
selectProject' = selectProjectForExisting (repoSharer repo) rid
selectRole =

View file

@ -604,6 +604,7 @@ instance AccountDB AccountPersistDB' where
Right sid -> do
ibid <- insert Inbox
obid <- insert Outbox
fsid <- insert FollowerSet
let defTime = UTCTime (ModifiedJulianDay 0) 0
person = Person
{ personIdent = sid
@ -618,6 +619,7 @@ instance AccountDB AccountPersistDB' where
, personAbout = ""
, personInbox = ibid
, personOutbox = obid
, personFollowers = fsid
}
pid <- insert person
return $ Right $ Entity pid person
@ -738,6 +740,8 @@ instance YesodBreadcrumbs App where
SharerOutboxItemR shr hid -> ( "#" <> keyHashidText hid
, Just $ SharerOutboxR shr
)
SharerFollowersR shr -> ("Followers", Just $ SharerR shr)
ActorKey1R -> ("Actor Key 1", Nothing)
ActorKey2R -> ("Actor Key 2", Nothing)

View file

@ -17,8 +17,10 @@ module Vervis.Handler.Inbox
( getInboxR
, getSharerInboxR
, getProjectInboxR
, getRepoInboxR
, postSharerInboxR
, postProjectInboxR
, postRepoInboxR
, getPublishR
, getSharerOutboxR
, getSharerOutboxItemR
@ -283,6 +285,15 @@ getProjectInboxR shr prj = getInbox here getInboxId
j <- getValBy404 $ UniqueProject prj sid
return $ projectInbox j
getRepoInboxR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoInboxR shr rp = getInbox here getInboxId
where
here = RepoInboxR shr rp
getInboxId = do
sid <- getKeyBy404 $ UniqueSharer shr
r <- getValBy404 $ UniqueRepo rp sid
return $ repoInbox r
postSharerInboxR :: ShrIdent -> Handler ()
postSharerInboxR shrRecip = do
federation <- getsYesod $ appFederation . appSettings
@ -326,6 +337,21 @@ postProjectInboxR shrRecip prjRecip = do
Left _ -> sendResponseStatus badRequest400 ()
Right _ -> return ()
postRepoInboxR :: ShrIdent -> RpIdent -> Handler ()
postRepoInboxR shrRecip rpRecip = do
federation <- getsYesod $ appFederation . appSettings
unless federation badMethod
contentTypes <- lookupHeaders "Content-Type"
now <- liftIO getCurrentTime
result <- runExceptT $ do
(auth, body) <- authenticateActivity now
(actbObject body,) <$>
handleRepoInbox now shrRecip rpRecip auth body
recordActivity now result contentTypes
case result of
Left _ -> sendResponseStatus badRequest400 ()
Right _ -> return ()
{-
jsonField :: (FromJSON a, ToJSON a) => Field Handler a
jsonField = checkMMap fromTextarea toTextarea textareaField

View file

@ -137,7 +137,7 @@ getPerson shr sharer person = do
, actorSummary = Nothing
, actorInbox = encodeRouteLocal $ SharerInboxR shr
, actorOutbox = Just $ encodeRouteLocal $ SharerOutboxR shr
, actorFollowers = Nothing
, actorFollowers = Just $ encodeRouteLocal $ SharerFollowersR shr
, actorPublicKeys =
[ Left $ encodeRouteLocal ActorKey1R
, Left $ encodeRouteLocal ActorKey2R

View file

@ -34,14 +34,18 @@ module Vervis.Handler.Repo
, deleteRepoDevR
, postRepoDevR
, getDarcsDownloadR
, getRepoTeamR
, getRepoFollowersR
, getHighlightStyleR
, postPostReceiveR
)
where
import Control.Exception hiding (Handler)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (logWarn)
import Data.Bifunctor
import Data.Git.Graph
import Data.Git.Harder
import Data.Git.Named (RefName (..))
@ -49,16 +53,16 @@ import Data.Git.Ref (toHex)
import Data.Git.Repository
import Data.Git.Storage (withRepo)
import Data.Git.Storage.Object (Object (..))
import Data.Git.Types (Blob (..), Commit (..), Person (..), entName)
import Data.Git.Types (Blob (..), Person (..), entName)
import Data.Graph.Inductive.Graph (noNodes)
import Data.Graph.Inductive.Query.Topsort
import Data.List (inits)
import Data.Text (Text, unpack)
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding
import Data.Text.Encoding.Error (lenientDecode)
import Data.Traversable (for)
import Database.Esqueleto hiding (delete, (%))
import Database.Persist (delete)
import Database.Persist
import Database.Persist.Sql
import Data.Hourglass (timeConvert)
import Formatting (sformat, stext, (%))
import System.Directory
@ -73,45 +77,62 @@ import Yesod.Form.Functions (runFormPost)
import Yesod.Form.Types (FormResult (..))
import Yesod.Persist.Core (runDB, getBy404)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.CaseInsensitive as CI (foldedCase)
import qualified Data.DList as D
import qualified Data.Set as S (member)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import qualified Database.Esqueleto as E
import Data.MediaType
import Web.ActivityPub hiding (Repo)
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite
import Yesod.RenderSource
import qualified Web.ActivityPub as AP
import Data.ByteString.Char8.Local (takeLine)
import Data.Either.Local
import Data.Git.Local
import Database.Persist.Local
import Text.FilePath.Local (breakExt)
import Yesod.Persist.Local
import qualified Data.Git.Local as G (createRepo)
import qualified Darcs.Local.Repository as D (createRepo)
import Vervis.API
import Vervis.Form.Repo
import Vervis.Foundation
import Vervis.Handler.Repo.Darcs
import Vervis.Handler.Repo.Git
import Vervis.Path
import Data.MediaType
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Model.Repo
import Vervis.Paginate
import Vervis.Readme
import Yesod.RenderSource
import Vervis.Settings
import Vervis.SourceTree
import Vervis.Style
import Vervis.Widget.Repo
import Vervis.Widget.Sharer
import qualified Darcs.Local.Repository as D (createRepo)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.Git.Local as G (createRepo)
import qualified Vervis.Formatting as F
import qualified Vervis.Hook as H
getReposR :: ShrIdent -> Handler Html
getReposR user = do
repos <- runDB $ select $ from $ \ (sharer, repo) -> do
where_ $
sharer ^. SharerIdent ==. val user &&.
sharer ^. SharerId ==. repo ^. RepoSharer
orderBy [asc $ repo ^. RepoIdent]
return $ repo ^. RepoIdent
repos <- runDB $ E.select $ E.from $ \ (sharer, repo) -> do
E.where_ $
sharer E.^. SharerIdent E.==. E.val user E.&&.
sharer E.^. SharerId E.==. repo E.^. RepoSharer
E.orderBy [E.asc $ repo E.^. RepoIdent]
return $ repo E.^. RepoIdent
defaultLayout $(widgetFile "repo/list")
postReposR :: ShrIdent -> Handler Html
@ -137,6 +158,8 @@ postReposR user = do
(rp2text $ nrpIdent nrp)
pid <- requireAuthId
runDB $ do
ibid <- insert Inbox
fsid <- insert FollowerSet
let repo = Repo
{ repoIdent = nrpIdent nrp
, repoSharer = sid
@ -146,6 +169,8 @@ postReposR user = do
, repoMainBranch = "master"
, repoCollabUser = Nothing
, repoCollabAnon = Nothing
, repoInbox = ibid
, repoFollowers = fsid
}
rid <- insert repo
let collab = RepoCollab
@ -175,14 +200,30 @@ selectRepo shar repo = do
Entity _rid r <- getBy404 $ UniqueRepo repo sid
return r
getRepoR :: ShrIdent -> RpIdent -> Handler Html
getRepoR shar repo = do
repository <- runDB $ selectRepo shar repo
case repoVcs repository of
VCSDarcs -> getDarcsRepoSource repository shar repo []
VCSGit ->
getGitRepoSource
repository shar repo (repoMainBranch repository) []
getRepoR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoR shr rp = do
repo <- runDB $ selectRepo shr rp
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let repoAP = AP.Repo
{ AP.repoActor = Actor
{ actorId = encodeRouteLocal $ RepoR shr rp
, actorType = ActorTypeRepo
, actorUsername = Nothing
, actorName = Just $ rp2text rp
, actorSummary = repoDesc repo
, actorInbox = encodeRouteLocal $ RepoInboxR shr rp
, actorOutbox = Nothing
, actorFollowers =
Just $ encodeRouteLocal $ RepoFollowersR shr rp
, actorPublicKeys = []
}
, AP.repoTeam = encodeRouteLocal $ RepoTeamR shr rp
}
dir = case repoVcs repo of
VCSDarcs -> []
VCSGit -> [repoMainBranch repo]
provideHtmlAndAP repoAP $ redirect $ RepoSourceR shr rp dir
putRepoR :: ShrIdent -> RpIdent -> Handler Html
putRepoR shr rp = do
@ -293,15 +334,15 @@ getRepoDevsR shr rp = do
Entity s _ <- getBy404 $ UniqueSharer shr
Entity r _ <- getBy404 $ UniqueRepo rp s
return r
select $ from $ \ (collab `InnerJoin`
person `InnerJoin`
sharer `LeftOuterJoin`
E.select $ E.from $ \ (collab `E.InnerJoin`
person `E.InnerJoin`
sharer `E.LeftOuterJoin`
role) -> do
on $ collab ^. RepoCollabRole ==. role ?. RoleId
on $ person ^. PersonIdent ==. sharer ^. SharerId
on $ collab ^. RepoCollabPerson ==. person ^. PersonId
where_ $ collab ^. RepoCollabRepo ==. val rid
return (sharer, role ?. RoleIdent)
E.on $ collab E.^. RepoCollabRole E.==. role E.?. RoleId
E.on $ person E.^. PersonIdent E.==. sharer E.^. SharerId
E.on $ collab E.^. RepoCollabPerson E.==. person E.^. PersonId
E.where_ $ collab E.^. RepoCollabRepo E.==. E.val rid
return (sharer, role E.?. RoleIdent)
defaultLayout $(widgetFile "repo/collab/list")
postRepoDevsR :: ShrIdent -> RpIdent -> Handler Html
@ -377,6 +418,53 @@ postRepoDevR shr rp dev = do
Just "DELETE" -> deleteRepoDevR shr rp dev
_ -> notFound
getRepoTeamR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoTeamR shr rp = do
memberShrs <- runDB $ do
sid <- getKeyBy404 $ UniqueSharer shr
_rid <- getKeyBy404 $ UniqueRepo rp sid
id_ <-
requireEitherAlt
(getKeyBy $ UniquePersonIdent sid)
(getKeyBy $ UniqueGroup sid)
"Found sharer that is neither person nor group"
"Found sharer that is both person and group"
case id_ of
Left pid -> return [shr]
Right gid -> do
pids <-
map (groupMemberPerson . entityVal) <$>
selectList [GroupMemberGroup ==. gid] []
sids <-
map (personIdent . entityVal) <$>
selectList [PersonId <-. pids] []
map (sharerIdent . entityVal) <$>
selectList [SharerId <-. sids] []
let here = RepoTeamR shr rp
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let team = Collection
{ collectionId = encodeRouteLocal here
, collectionType = CollectionTypeUnordered
, collectionTotalItems = Just $ length memberShrs
, collectionCurrent = Nothing
, collectionFirst = Nothing
, collectionLast = Nothing
, collectionItems = map (encodeRouteHome . SharerR) memberShrs
}
provideHtmlAndAP team $ redirectToPrettyJSON here
getRepoFollowersR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoFollowersR shr rp = getFollowersCollection here getFsid
where
here = RepoFollowersR shr rp
getFsid = do
sid <- getKeyBy404 $ UniqueSharer shr
r <- getValBy404 $ UniqueRepo rp sid
return $ repoFollowers r
getHighlightStyleR :: Text -> Handler TypedContent
getHighlightStyleR styleName =
case lookup (unpack styleName) highlightingStyles of
@ -384,5 +472,108 @@ getHighlightStyleR styleName =
Just style ->
return $ TypedContent typeCss $ toContent $ styleToCss style
postPostReceiveR :: Handler ()
postPostReceiveR = error "TODO post-receive handler not implemented yet"
postPostReceiveR :: Handler Text
postPostReceiveR = do
push <- requireCheckJsonBody
(pushAP, shr, rp) <- push2ap push
user <- runDB $ do
p <- getJustEntity $ toSqlKey $ H.pushUser push
s <- getJust $ personIdent $ entityVal p
return (p, s)
let shrUser = sharerIdent $ snd user
summary <- do
let mbranch = H.pushBranch push
total = pushCommitsTotal pushAP
withUrlRenderer
[hamlet|
<p>
<a href=@{SharerR shrUser}>#{shr2text shrUser}
\ pushed #{total} #
\ #{commitsText mbranch total} to repo #
<a href=@{RepoR shr rp}>#{rp2text rp}</a>^{branchText shr rp mbranch}.
|]
eid <- pushCommitsC user summary pushAP shr rp
case eid of
Left e -> liftIO $ throwIO $ userError $ T.unpack e
Right obiid -> do
renderUrl <- askUrlRender
obikhid <- encodeKeyHashid obiid
return $
"Push activity published: " <>
renderUrl (SharerOutboxItemR shrUser obikhid)
where
push2ap (H.Push secret _ sharer repo mbranch mbefore after early mlate) = do
encodeRouteLocal <- getEncodeRouteLocal
let shr = text2shr sharer
rp = text2rp repo
commit2ap' = commit2ap shr rp
(commitsLast, commitsFirst) <-
runDB $ case mlate of
Nothing -> (,) <$> traverse commit2ap' early <*> pure Nothing
Just (_omitted, late) ->
(,) <$> traverse commit2ap' late
<*> (Just <$> traverse commit2ap' early)
return
( Push
{ pushCommitsLast = commitsLast
, pushCommitsFirst = commitsFirst
, pushCommitsTotal =
case mlate of
Nothing -> length early
Just (omitted, late) ->
length early + omitted + length late
, pushTarget =
encodeRouteLocal $
case mbranch of
Nothing -> RepoR shr rp
Just b -> RepoBranchR shr rp b
, pushHashBefore = mbefore
, pushHashAfter = after
}
, shr
, rp
)
where
commit2ap shr rp (H.Commit (wauthor, wtime) mcommitted hash title desc) = do
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
author <- authorByEmail wauthor
mcommitter <- traverse (authorByEmail . fst) mcommitted
return Commit
{ commitId = encodeRouteLocal $ RepoPatchR shr rp hash
, commitRepository = encodeRouteLocal $ RepoR shr rp
, commitAuthor = second (encodeRouteHome . SharerR) author
, commitCommitter =
second (encodeRouteHome . SharerR) <$> mcommitter
, commitTitle = title
, commitHash = Hash $ encodeUtf8 hash
, commitDescription =
if T.null desc
then Nothing
else Just desc
, commitWritten = wtime
, commitCommitted = snd <$> mcommitted
}
where
authorByEmail (H.Author name email) = do
mperson <- getValBy $ UniquePersonEmail email
case mperson of
Nothing -> return $ Left $ Author name email
Just person ->
Right . sharerIdent <$> getJust (personIdent person)
commitsText :: Maybe a -> Int -> Text
commitsText Nothing n =
if n > 1
then "patches"
else "patch"
commitsText (Just _) n =
if n > 1
then "commits"
else "commit"
--branchText :: ShrIdent -> RpIdent -> Maybe Text -> HtmlUrl (Route App)
branchText _ _ Nothing = const mempty
branchText shr rp (Just branch) =
[hamlet|
, branch #
<a href=@{RepoBranchR shr rp branch}>#{branch}
|]

View file

@ -48,7 +48,7 @@ import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType
import Web.ActivityPub
import Web.ActivityPub hiding (Repo)
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.RenderSource

View file

@ -59,7 +59,7 @@ import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType
import Web.ActivityPub hiding (Commit, Author)
import Web.ActivityPub hiding (Commit, Author, Repo)
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.RenderSource

View file

@ -16,6 +16,7 @@
module Vervis.Handler.Sharer
( getSharersR
, getSharerR
, getSharerFollowersR
)
where
@ -30,6 +31,10 @@ import Yesod.Core.Content (TypedContent)
import Yesod.Core.Handler (redirect, notFound)
import Yesod.Persist.Core (runDB, getBy404)
import Database.Persist.Local
import Yesod.Persist.Local
import Vervis.API
import Vervis.Foundation
import Vervis.Handler.Person
import Vervis.Handler.Group
@ -64,3 +69,21 @@ getSharerR shr = do
case ent of
Left (Entity _ p) -> getPerson shr s p
Right (Entity _ g) -> getGroup shr g
getSharerFollowersR :: ShrIdent -> Handler TypedContent
getSharerFollowersR shr = getFollowersCollection here getFsid
where
here = SharerFollowersR shr
getFsid = do
sid <- getKeyBy404 $ UniqueSharer shr
mval <- runMaybeT
$ Left <$> MaybeT (getValBy $ UniquePersonIdent sid)
<|> Right <$> MaybeT (getValBy $ UniqueGroup sid)
case mval of
Nothing -> do
$logWarn $ "Found non-person non-group sharer: " <> shr2text shr
notFound
Just val ->
case val of
Left person -> return $ personFollowers person
Right _group -> notFound

View file

@ -42,6 +42,7 @@ import Data.Git.Graph
import Data.Git.Harder
import Data.Graph.Inductive.Graph -- (noNodes)
import Data.Graph.Inductive.Query.Topsort
import Data.Int
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Text (Text)
import Data.Time.Clock
@ -49,6 +50,7 @@ import Data.Time.Clock.POSIX
import Data.Word
import GHC.Generics
import Network.HTTP.Client
import Network.HTTP.Types.Header
import System.Directory
import System.Environment
import System.Exit
@ -57,6 +59,7 @@ import System.IO
import Text.Email.Aeson.Instances ()
import Text.Email.Validate
import Time.Types
import Yesod.Core.Content
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16
@ -122,9 +125,12 @@ instance ToJSON Commit
data Push = Push
{ pushSecret :: Text
, pushUser :: Int64
, pushSharer :: Text
, pushRepo :: Text
, pushBranch :: Maybe Text
, pushBefore :: Maybe Text
, pushAfter :: Text
, pushInit :: NonEmpty Commit
, pushLast :: Maybe (Int, NonEmpty Commit)
}
@ -148,10 +154,11 @@ writeHookConfig config = do
reportNewCommits :: Config -> Text -> Text -> IO ()
reportNewCommits config sharer repo = do
user <- read <$> getEnv "VERVIS_SSH_USER"
manager <- newManager defaultManagerSettings
withRepo "." $ loop manager
withRepo "." $ loop user manager
where
loop manager git = do
loop user manager git = do
eof <- isEOF
unless eof $ do
result <- runExceptT $ do
@ -205,9 +212,12 @@ reportNewCommits config sharer repo = do
return (eNE, Just (middle, lNE))
let push = Push
{ pushSecret = configSecret config
, pushUser = user
, pushSharer = sharer
, pushRepo = repo
, pushBranch = Just branch
, pushBefore = old <$ moldRef
, pushAfter = new
, pushInit = early
, pushLast = late
}
@ -219,6 +229,7 @@ reportNewCommits config sharer repo = do
req <- requestFromURI $ uriFromObjURI uri
let req' =
setRequestCheckStatus $
consHeader hContentType typeJson $
req { method = "POST"
, requestBody = RequestBodyLBS $ encode push
}
@ -227,10 +238,11 @@ reportNewCommits config sharer repo = do
case result of
Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e
Right _resp -> return ()
loop manager git
loop user manager git
where
adaptErr :: HttpException -> Text
adaptErr = T.pack . displayException
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
parseRef t =
if t == nullRef
then return Nothing

View file

@ -1008,6 +1008,48 @@ changes hLocal ctx =
"summary"
-- 129
, addFieldPrimRequired "TicketDependency" defaultTime "created"
-- 130
, addFieldRefRequired'
"Repo"
FollowerSet130
(Just $ do
rids <- selectKeysList ([] :: [Filter Repo130]) []
for_ rids $ \ rid -> do
fsid <- insert FollowerSet130
update rid [Repo130Followers =. fsid]
)
"followers"
"FollowerSet"
-- 131
, addUnique "Repo" $ Unique "UniqueRepoFollowers" ["followers"]
-- 132
, addFieldRefRequired'
"Repo"
Inbox130
(Just $ do
rids <- selectKeysList ([] :: [Filter Repo130]) []
for_ rids $ \ rid -> do
ibid <- insert Inbox130
update rid [Repo130Inbox =. ibid]
)
"inbox"
"Inbox"
-- 133
, addUnique "Repo" $ Unique "UniqueRepoInbox" ["inbox"]
-- 134
, addFieldRefRequired'
"Person"
FollowerSet130
(Just $ do
pids <- selectKeysList ([] :: [Filter Person130]) []
for_ pids $ \ pid -> do
fsid <- insert FollowerSet130
update pid [Person130Followers =. fsid]
)
"followers"
"FollowerSet"
-- 135
, addUnique "Person" $ Unique "UniquePersonFollowers" ["followers"]
]
migrateDB

View file

@ -118,6 +118,10 @@ module Vervis.Migration.Model
, Ticket127Generic (..)
, TicketDependency127Generic (..)
, TicketDependency127
, Inbox130Generic (..)
, FollowerSet130Generic (..)
, Repo130
, Person130
)
where
@ -239,3 +243,6 @@ makeEntitiesMigration "20190624"
makeEntitiesMigration "127"
$(modelFile "migrations/2019_07_11.model")
makeEntitiesMigration "130"
$(modelFile "migrations/2019_09_06.model")

View file

@ -42,6 +42,7 @@ import Network.SSH.Channel
import Network.SSH.Crypto
import Network.SSH.Session
import System.Directory (doesFileExist, doesDirectoryExist)
import System.Environment
import System.FilePath ((</>))
import System.Process (CreateProcess (..), StdStream (..), createProcess, proc)
@ -263,6 +264,8 @@ runAction repoDir _wantReply action =
can <- canPushTo sharer repo
if can
then whenGitRepoExists True repoPath $ do
pid <- authId <$> askAuthDetails
liftIO $ setEnv "VERVIS_SSH_USER" (show $ fromSqlKey pid)
execute "git-receive-pack" [repoPath]
return ARProcess
else return $ ARFail "You can't push to this repository"

View file

@ -31,6 +31,7 @@ module Web.ActivityPub
, Owner (..)
, PublicKey (..)
, Actor (..)
, Repo (..)
, Project (..)
, CollectionType (..)
, Collection (..)
@ -175,7 +176,8 @@ instance (ActivityPub a, UriMode u) => ToJSON (Doc a u) where
context [t] = "@context" .= t
context ts = "@context" .= ts
data ActorType = ActorTypePerson | ActorTypeProject | ActorTypeOther Text
data ActorType =
ActorTypePerson | ActorTypeRepo | ActorTypeProject | ActorTypeOther Text
deriving Eq
instance FromJSON ActorType where
@ -183,6 +185,7 @@ instance FromJSON ActorType where
where
parse t
| t == "Person" = ActorTypePerson
| t == "Repository" = ActorTypeRepo
| t == "Project" = ActorTypeProject
| otherwise = ActorTypeOther t
@ -191,6 +194,7 @@ instance ToJSON ActorType where
toEncoding at =
toEncoding $ case at of
ActorTypePerson -> "Person"
ActorTypeRepo -> "Repository"
ActorTypeProject -> "Project"
ActorTypeOther t -> t
@ -307,6 +311,24 @@ instance ActivityPub Actor where
<> "followers" .=? (ObjURI authority <$> followers)
<> "publicKey" `pair` encodePublicKeySet authority pkeys
data Repo u = Repo
{ repoActor :: Actor u
, repoTeam :: LocalURI
}
instance ActivityPub Repo where
jsonldContext _ = [as2Context, secContext, forgeContext, extContext]
parseObject o = do
(h, a) <- parseObject o
unless (actorType a == ActorTypeRepo) $
fail "Actor type isn't Repository"
fmap (h,) $
Repo a
<$> withAuthorityO h (o .:| "team")
toSeries authority (Repo actor team)
= toSeries authority actor
<> "team" .= ObjURI authority team
data Project u = Project
{ projectActor :: Actor u
, projectTeam :: LocalURI
@ -1021,10 +1043,11 @@ encodeOffer authority actor (Offer obj target)
<> "target" .= target
data Push u = Push
{ pushCommits :: NonEmpty (Commit u)
{ pushCommitsLast :: NonEmpty (Commit u)
, pushCommitsFirst :: Maybe (NonEmpty (Commit u))
, pushCommitsTotal :: Int
, pushTarget :: LocalURI
, pushHashBefore :: Text
, pushHashBefore :: Maybe Text
, pushHashAfter :: Text
}
@ -1033,23 +1056,25 @@ parsePush a o = do
c <- o .: "object"
Push
<$> (traverse (withAuthorityT a . parseObject) =<< c .: "items")
<*> (traverse (traverse $ withAuthorityT a . parseObject) =<< c .:? "earlyItems")
<*> c .: "totalItems"
<*> withAuthorityO a (o .: "target")
<*> o .: "hashBefore"
<*> o .:? "hashBefore"
<*> o .: "hashAfter"
encodePush :: UriMode u => Authority u -> Push u -> Series
encodePush a (Push commits total target before after)
encodePush a (Push lateCommits earlyCommits total target before after)
= "object" `pair` pairs
( "type" .= ("OrderedCollection" :: Text)
<> pair
"items"
(listEncoding (pairs . toSeries a) (NE.toList commits))
<> pair "items" (objectList lateCommits)
<> maybe mempty (pair "earlyItems" . objectList) earlyCommits
<> "totalItems" .= total
)
<> "target" .= ObjURI a target
<> "hashBefore" .= before
<> "hashBefore" .=? before
<> "hashAfter" .= after
where
objectList items = listEncoding (pairs . toSeries a) (NE.toList items)
data Reject u = Reject
{ rejectObject :: ObjURI u

View file

@ -16,7 +16,7 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<tr>
<th>Collaborator
<th>Role
$forall (Entity _sid sharer, Value mrl) <- devs
$forall (Entity _sid sharer, E.Value mrl) <- devs
<tr>
<td>^{sharerLinkW sharer}
<td>

View file

@ -15,7 +15,7 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<p>These are the repositories shared by #{shr2text user}.
<ul>
$forall Value repo <- repos
$forall E.Value repo <- repos
<li>
<a href=@{RepoR user repo}>#{rp2text repo}
<li>