Provide AP representation of commits, and support committer field

This commit is contained in:
fr33domlover 2019-08-06 13:23:11 +00:00
parent 50614359ab
commit 2c18660a3b
10 changed files with 238 additions and 44 deletions

View file

@ -24,6 +24,7 @@ module Data.Aeson.Local
, (.=?) , (.=?)
, (.=%) , (.=%)
, (.=+) , (.=+)
, (.=+?)
, WithValue (..) , WithValue (..)
) )
where where
@ -87,6 +88,11 @@ infixr 8 .=+
k .=+ Left x = k .= x k .=+ Left x = k .= x
k .=+ Right y = k .= y k .=+ Right y = k .= y
infixr 8 .=+?
(.=+?) :: (ToJSON a, ToJSON b) => Text -> Maybe (Either a b) -> Series
k .=+? Nothing = mempty
k .=+? (Just v) = k .=+ v
data WithValue a = WithValue data WithValue a = WithValue
{ wvRaw :: Object { wvRaw :: Object
, wvParsed :: a , wvParsed :: a

View file

@ -39,6 +39,7 @@ module Vervis.ActivityPub
, getProjectAndDeps , getProjectAndDeps
, deliverRemoteDB' , deliverRemoteDB'
, deliverRemoteHttp , deliverRemoteHttp
, serveCommit
) )
where where
@ -88,7 +89,7 @@ import Yesod.HttpSignature
import Database.Persist.JSON import Database.Persist.JSON
import Network.FedURI import Network.FedURI
import Network.HTTP.Digest import Network.HTTP.Digest
import Web.ActivityPub import Web.ActivityPub hiding (Author (..))
import Yesod.ActivityPub import Yesod.ActivityPub
import Yesod.MonadSite import Yesod.MonadSite
import Yesod.FedURI import Yesod.FedURI
@ -106,8 +107,12 @@ import Vervis.FedURI
import Vervis.Foundation import Vervis.Foundation
import Vervis.Model import Vervis.Model
import Vervis.Model.Ident import Vervis.Model.Ident
import Vervis.Patch
import Vervis.RemoteActorStore import Vervis.RemoteActorStore
import Vervis.Settings import Vervis.Settings
import Vervis.Time
import Vervis.Widget.Repo
import Vervis.Widget.Sharer
hostIsLocal :: (MonadSite m, SiteEnv m ~ App) => Host -> m Bool hostIsLocal :: (MonadSite m, SiteEnv m ~ App) => Host -> m Bool
hostIsLocal h = asksSite $ (== h) . appInstanceHost . appSettings hostIsLocal h = asksSite $ (== h) . appInstanceHost . appSettings
@ -637,3 +642,51 @@ deliverRemoteHttp hContext obid doc (fetched, unfetched, unknown) = do
Right _ -> delete udlid Right _ -> delete udlid
where where
logDebug'' t = logDebug' $ T.concat ["deliverUnfetched ", renderAuthority h, t] logDebug'' t = logDebug' $ T.concat ["deliverUnfetched ", renderAuthority h, t]
serveCommit
:: ShrIdent
-> RpIdent
-> Text
-> Patch
-> [Text]
-> Handler TypedContent
serveCommit shr rp ref patch parents = do
(msharerWritten, msharerCommitted) <- runDB $ (,)
<$> getSharer (patchWritten patch)
<*> maybe (pure Nothing) getSharer (patchCommitted patch)
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let (author, written) = patchWritten patch
mcommitter = patchCommitted patch
patchAP = AP.Commit
{ commitId =
encodeRouteLocal $ RepoPatchR shr rp ref
, commitRepository = encodeRouteLocal $ RepoR shr rp
, commitAuthor =
makeAuthor encodeRouteHome msharerWritten author
, commitCommitter =
makeAuthor encodeRouteHome msharerCommitted . fst <$>
mcommitter
, commitTitle = patchTitle patch
, commitHash = Hash $ encodeUtf8 ref
, commitDescription =
let desc = patchDescription patch
in if T.null desc
then Nothing
else Just desc
, commitWritten = written
, commitCommitted = snd <$> patchCommitted patch
}
provideHtmlAndAP patchAP $
let number = zip ([1..] :: [Int])
in $(widgetFile "repo/patch")
where
getSharer (author, _time) = do
mp <- getBy $ UniquePersonEmail $ authorEmail author
for mp $ \ (Entity _ person) -> getJust $ personIdent person
makeAuthor _ Nothing author = Left AP.Author
{ AP.authorName = authorName author
, AP.authorEmail = authorEmail author
}
makeAuthor encodeRouteHome (Just sharer) _ =
Right $ encodeRouteHome $ SharerR $ sharerIdent sharer

View file

@ -279,9 +279,14 @@ readPatch path hash = do
either error id $ either error id $
A.parseOnly (author <* A.endOfInput) $ piAuthor pi A.parseOnly (author <* A.endOfInput) $ piAuthor pi
return Patch return Patch
{ patchAuthorName = an { patchWritten =
, patchAuthorEmail = ae ( Author
, patchTime = piTime pi { authorName = an
, authorEmail = ae
}
, piTime pi
)
, patchCommitted = Nothing
, patchTitle = piTitle pi , patchTitle = piTitle pi
, patchDescription = fromMaybe "" $ piDescription pi , patchDescription = fromMaybe "" $ piDescription pi
, patchDiff = , patchDiff =

View file

@ -1,6 +1,6 @@
{- This file is part of Vervis. {- This file is part of Vervis.
- -
- Written in 2016, 2018 by fr33domlover <fr33domlover@riseup.net>. - Written in 2016, 2018, 2019 by fr33domlover <fr33domlover@riseup.net>.
- -
- Copying is an act of love. Please copy, reuse and share. - Copying is an act of love. Please copy, reuse and share.
- -
@ -195,15 +195,11 @@ listRefs path = G.withRepo (fromString path) $ \ git ->
patch :: [Edit] -> Commit SHA1 -> Patch patch :: [Edit] -> Commit SHA1 -> Patch
patch edits c = Patch patch edits c = Patch
{ patchAuthorName = decodeUtf8 $ personName $ commitAuthor c { patchWritten = makeAuthor $ commitAuthor c
, patchAuthorEmail = , patchCommitted =
let b = personEmail $ commitAuthor c if commitAuthor c == commitCommitter c
in case emailAddress b of then Nothing
Nothing -> error $ "Invalid email " ++ T.unpack (decodeUtf8 b) else Just $ makeAuthor $ commitCommitter c
Just e -> e
, patchTime =
let Elapsed (Seconds t) = gitTimeUTC $ personTime $ commitAuthor c
in posixSecondsToUTCTime $ fromIntegral t
, patchTitle = title , patchTitle = title
, patchDescription = desc , patchDescription = desc
, patchDiff = edits , patchDiff = edits
@ -214,6 +210,19 @@ patch edits c = Patch
in (T.strip l, T.strip r) in (T.strip l, T.strip r)
(title, desc) = split $ decodeUtf8 $ commitMessage c (title, desc) = split $ decodeUtf8 $ commitMessage c
makeAuthor (Person name email time) =
( Author
{ authorName = decodeUtf8 name
, authorEmail =
case emailAddress email of
Nothing ->
error $ "Invalid email " ++ T.unpack (decodeUtf8 email)
Just e -> e
}
, let Elapsed (Seconds t) = gitTimeUTC time
in posixSecondsToUTCTime $ fromIntegral t
)
ep2fp :: EntPath -> FilePath ep2fp :: EntPath -> FilePath
ep2fp = T.unpack . decodeUtf8 . B.intercalate "/" . map getEntNameBytes ep2fp = T.unpack . decodeUtf8 . B.intercalate "/" . map getEntNameBytes

View file

@ -263,7 +263,7 @@ getRepoChangesR shar repo ref = do
VCSDarcs -> getDarcsRepoChanges shar repo ref VCSDarcs -> getDarcsRepoChanges shar repo ref
VCSGit -> getGitRepoChanges shar repo ref VCSGit -> getGitRepoChanges shar repo ref
getRepoPatchR :: ShrIdent -> RpIdent -> Text -> Handler Html getRepoPatchR :: ShrIdent -> RpIdent -> Text -> Handler TypedContent
getRepoPatchR shr rp ref = do getRepoPatchR shr rp ref = do
repository <- runDB $ selectRepo shr rp repository <- runDB $ selectRepo shr rp
case repoVcs repository of case repoVcs repository of

View file

@ -26,7 +26,7 @@ import Control.Monad.IO.Class (liftIO)
import Data.List (inits) import Data.List (inits)
import Data.Maybe import Data.Maybe
import Data.Text (Text, unpack) import Data.Text (Text, unpack)
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Traversable (for) import Data.Traversable (for)
import Database.Esqueleto import Database.Esqueleto
@ -41,25 +41,34 @@ import Yesod.Persist.Core (runDB, get404)
import Yesod.AtomFeed (atomFeed) import Yesod.AtomFeed (atomFeed)
import Yesod.RssFeed (rssFeed) import Yesod.RssFeed (rssFeed)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.DList as D import qualified Data.DList as D
import qualified Data.Set as S (member) import qualified Data.Set as S (member)
import qualified Data.Text as T (unpack) import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With) import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType
import Web.ActivityPub
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.RenderSource
import Data.ByteString.Char8.Local (takeLine) import Data.ByteString.Char8.Local (takeLine)
import Text.FilePath.Local (breakExt) import Text.FilePath.Local (breakExt)
import qualified Darcs.Local.Repository as D (createRepo)
import Vervis.ActivityPub
import Vervis.ChangeFeed (changeFeed) import Vervis.ChangeFeed (changeFeed)
import Vervis.Form.Repo import Vervis.Form.Repo
import Vervis.Foundation import Vervis.Foundation
import Vervis.Path import Vervis.Path
import Data.MediaType
import Vervis.Model import Vervis.Model
import Vervis.Model.Ident import Vervis.Model.Ident
import Vervis.Model.Repo import Vervis.Model.Repo
import Vervis.Paginate import Vervis.Paginate
import Vervis.Patch import Vervis.Patch
import Vervis.Readme import Vervis.Readme
import Yesod.RenderSource
import Vervis.Settings import Vervis.Settings
import Vervis.SourceTree import Vervis.SourceTree
import Vervis.Style import Vervis.Style
@ -68,8 +77,6 @@ import Vervis.Widget (buttonW)
import Vervis.Widget.Repo import Vervis.Widget.Repo
import Vervis.Widget.Sharer import Vervis.Widget.Sharer
import qualified Darcs.Local.Repository as D (createRepo)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Vervis.Darcs as D (readSourceView, readChangesView, readPatch) import qualified Vervis.Darcs as D (readSourceView, readChangesView, readPatch)
getDarcsRepoSource :: Repo -> ShrIdent -> RpIdent -> [Text] -> Handler Html getDarcsRepoSource :: Repo -> ShrIdent -> RpIdent -> [Text] -> Handler Html
@ -120,16 +127,10 @@ getDarcsDownloadR shar repo dir = do
then sendFile typeOctet filePath then sendFile typeOctet filePath
else notFound else notFound
getDarcsPatch :: ShrIdent -> RpIdent -> Text -> Handler Html getDarcsPatch :: ShrIdent -> RpIdent -> Text -> Handler TypedContent
getDarcsPatch shr rp ref = do getDarcsPatch shr rp ref = do
path <- askRepoDir shr rp path <- askRepoDir shr rp
mpatch <- liftIO $ D.readPatch path ref mpatch <- liftIO $ D.readPatch path ref
case mpatch of case mpatch of
Nothing -> notFound Nothing -> notFound
Just patch -> do Just patch -> serveCommit shr rp ref patch []
let parents = [] :: [Text]
msharer <- runDB $ do
mp <- getBy $ UniquePersonEmail $ patchAuthorEmail patch
for mp $ \ (Entity _ person) -> get404 $ personIdent person
let number = zip ([1..] :: [Int])
defaultLayout $(widgetFile "repo/patch")

View file

@ -35,7 +35,7 @@ import Data.Graph.Inductive.Query.Topsort
import Data.List (inits) import Data.List (inits)
import Data.Maybe import Data.Maybe
import Data.Text (Text, unpack) import Data.Text (Text, unpack)
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Traversable (for) import Data.Traversable (for)
import Database.Esqueleto import Database.Esqueleto
@ -53,23 +53,32 @@ import Yesod.RssFeed (rssFeed)
import qualified Data.DList as D import qualified Data.DList as D
import qualified Data.Set as S (member) import qualified Data.Set as S (member)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With) import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType
import Web.ActivityPub hiding (Commit, Author)
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.RenderSource
import qualified Web.ActivityPub as AP
import Data.ByteString.Char8.Local (takeLine) import Data.ByteString.Char8.Local (takeLine)
import Data.Git.Local import Data.Git.Local
import Text.FilePath.Local (breakExt) import Text.FilePath.Local (breakExt)
import Vervis.ActivityPub
import Vervis.ChangeFeed (changeFeed) import Vervis.ChangeFeed (changeFeed)
import Vervis.Form.Repo import Vervis.Form.Repo
import Vervis.Foundation import Vervis.Foundation
import Vervis.Path import Vervis.Path
import Data.MediaType
import Vervis.Model import Vervis.Model
import Vervis.Model.Ident import Vervis.Model.Ident
import Vervis.Model.Repo import Vervis.Model.Repo
import Vervis.Paginate import Vervis.Paginate
import Vervis.Patch import Vervis.Patch
import Vervis.Readme import Vervis.Readme
import Yesod.RenderSource
import Vervis.Settings import Vervis.Settings
import Vervis.SourceTree import Vervis.SourceTree
import Vervis.Style import Vervis.Style
@ -122,12 +131,8 @@ getGitRepoChanges shar repo ref = do
provideRep $ rssFeed feed provideRep $ rssFeed feed
else notFound else notFound
getGitPatch :: ShrIdent -> RpIdent -> Text -> Handler Html getGitPatch :: ShrIdent -> RpIdent -> Text -> Handler TypedContent
getGitPatch shr rp ref = do getGitPatch shr rp ref = do
path <- askRepoDir shr rp path <- askRepoDir shr rp
(patch, parents) <- liftIO $ G.readPatch path ref (patch, parents) <- liftIO $ G.readPatch path ref
msharer <- runDB $ do serveCommit shr rp ref patch parents
mp <- getBy $ UniquePersonEmail $ patchAuthorEmail patch
for mp $ \ (Entity _ person) -> get404 $ personIdent person
let number = zip ([1..] :: [Int])
defaultLayout $(widgetFile "repo/patch")

View file

@ -21,6 +21,7 @@
module Vervis.Patch module Vervis.Patch
( Hunk (..) ( Hunk (..)
, Edit (..) , Edit (..)
, Author (..)
, Patch (..) , Patch (..)
) )
where where
@ -51,10 +52,14 @@ data Edit
| TextToBinary FilePath [Text] Word32 Int64 Word32 | TextToBinary FilePath [Text] Word32 Int64 Word32
| BinaryToText FilePath Int64 Word32 [Text] Word32 | BinaryToText FilePath Int64 Word32 [Text] Word32
data Author = Author
{ authorName :: Text
, authorEmail :: EmailAddress
}
data Patch = Patch data Patch = Patch
{ patchAuthorName :: Text { patchWritten :: (Author, UTCTime)
, patchAuthorEmail :: EmailAddress , patchCommitted :: Maybe (Author, UTCTime)
, patchTime :: UTCTime
, patchTitle :: Text , patchTitle :: Text
, patchDescription :: Text , patchDescription :: Text
, patchDiff :: [Edit] , patchDiff :: [Edit]

View file

@ -45,6 +45,9 @@ module Web.ActivityPub
, TextPandocMarkdown (..) , TextPandocMarkdown (..)
, TicketLocal (..) , TicketLocal (..)
, Ticket (..) , Ticket (..)
, Author (..)
, Hash (..)
, Commit (..)
-- * Activity -- * Activity
, Accept (..) , Accept (..)
@ -91,6 +94,7 @@ import Data.Aeson.Encoding (pair)
import Data.Aeson.Types (Parser, typeMismatch, listEncoding) import Data.Aeson.Types (Parser, typeMismatch, listEncoding)
import Data.Bifunctor import Data.Bifunctor
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Char
import Data.Foldable (for_) import Data.Foldable (for_)
import Data.List import Data.List
import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty (NonEmpty (..))
@ -104,18 +108,21 @@ import Network.HTTP.Client hiding (Proxy, proxy)
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither) import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
import Network.HTTP.Simple (JSONException) import Network.HTTP.Simple (JSONException)
import Network.HTTP.Types.Header (HeaderName, hContentType) import Network.HTTP.Types.Header (HeaderName, hContentType)
import Text.Email.Parser (EmailAddress)
import Text.HTML.SanitizeXSS import Text.HTML.SanitizeXSS
import Yesod.Core.Content (ContentType) import Yesod.Core.Content (ContentType)
import Yesod.Core.Handler (ProvidedRep, provideRepType) import Yesod.Core.Handler (ProvidedRep, provideRepType)
import Network.HTTP.Client.Signature import Network.HTTP.Client.Signature
import qualified Data.Attoparsec.ByteString as A
import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Network.HTTP.Signature as S import qualified Network.HTTP.Signature as S
import qualified Text.Email.Parser as E
import Crypto.PublicVerifKey import Crypto.PublicVerifKey
import Network.FedURI import Network.FedURI
@ -821,6 +828,100 @@ instance ActivityPub Ticket where
<> "assignedTo" .=? assignedTo <> "assignedTo" .=? assignedTo
<> "isResolved" .= isResolved <> "isResolved" .= isResolved
data Author = Author
{ authorName :: Text
, authorEmail :: EmailAddress
}
instance FromJSON Author where
parseJSON = withObject "Author" $ \ o ->
Author
<$> o .: "name"
<*> (parseMailto =<< o .: "mbox")
where
parseMailto =
either fail return .
A.parseOnly (A.string "mailto:" *> E.addrSpec <* A.endOfInput) .
encodeUtf8
instance ToJSON Author where
toJSON = error "toJSON Author"
toEncoding (Author name email) =
pairs
$ "name" .= name
<> "mbox" .= ("mailto:" <> decodeUtf8 (E.toByteString email))
newtype Hash = Hash ByteString
instance FromJSON Hash where
parseJSON = withText "Hash" $ \ t ->
let b = encodeUtf8 t
in if not (BC.null b) && BC.all isHexDigit b
then return $ Hash b
else fail "Hash should be a non-empty hex string"
instance ToJSON Hash where
toJSON (Hash b) = toJSON $ decodeUtf8 b
toEncoding (Hash b) = toEncoding $ decodeUtf8 b
data Commit u = Commit
{ commitId :: LocalURI
, commitRepository :: LocalURI
, commitAuthor :: Either Author (ObjURI u)
, commitCommitter :: Maybe (Either Author (ObjURI u))
, commitTitle :: Text
, commitHash :: Hash
, commitDescription :: Maybe Text
, commitWritten :: UTCTime
, commitCommitted :: Maybe UTCTime
}
instance ActivityPub Commit where
jsonldContext _ = [as2Context, forgeContext, extContext]
parseObject o = do
typ <- o .: "type"
unless (typ == ("Commit" :: Text)) $
fail "type isn't Commit"
mdesc <- o .:? "description"
mdescContent <- for mdesc $ \ desc -> do
descType <- desc .: "mediaType"
unless (descType == ("text/plain" :: Text)) $
fail "description mediaType isn't \"text/plain\""
desc .: "content"
ObjURI a id_ <- o .: "id"
fmap (a,) $
Commit id_
<$> withAuthorityO a (o .: "repository")
<*> o .:+ "attributedTo"
<*> o .:+? "committedBy"
<*> o .: "name"
<*> o .: "hash"
<*> pure mdescContent
<*> o .: "created"
<*> o .:? "committed"
toSeries authority
(Commit id_ repo author committer title hash mdesc written mcommitted)
= "id" .= ObjURI authority id_
<> "type" .= ("Commit" :: Text)
<> "repository" .= ObjURI authority repo
<> "attributedTo" .=+ author
<> "committedBy" .=+? committer
<> "name" .= title
<> "hash" .= hash
<> maybe
mempty
(\ desc -> "description" .= object
[ "content" .= desc
, "mediaType" .= ("text/plain" :: Text)
]
)
mdesc
<> "created" .= written
<> "committed" .=? mcommitted
data Accept u = Accept data Accept u = Accept
{ acceptObject :: ObjURI u { acceptObject :: ObjURI u
, acceptResult :: LocalURI , acceptResult :: LocalURI

View file

@ -16,13 +16,22 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<tr> <tr>
<td>By <td>By
<td> <td>
$maybe sharer <- msharer $maybe sharer <- msharerWritten
^{sharerLinkW sharer} ^{sharerLinkW sharer}
$nothing $nothing
#{patchAuthorName patch} #{authorName author}
$maybe (committer, _) <- patchCommitted patch
;
$maybe sharer <- msharerCommitted
^{sharerLinkW sharer}
$nothing
#{authorName committer}
<tr> <tr>
<td>At <td>At
<td>#{showDate $ patchTime patch} <td>
#{showDate written}
$maybe (_, committed) <- patchCommitted patch
; #{showDate committed}
<tr> <tr>
<td>Title <td>Title
<td>#{patchTitle patch} <td>#{patchTitle patch}