2019-01-22 00:54:57 +09:00
|
|
|
{- This file is part of Vervis.
|
|
|
|
-
|
2022-06-23 18:09:02 +09:00
|
|
|
- Written in 2019, 2020, 2021, 2022 by fr33domlover <fr33domlover@riseup.net>.
|
2019-01-22 00:54:57 +09:00
|
|
|
-
|
|
|
|
- ♡ 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 Web.ActivityPub
|
2019-02-22 08:59:53 +09:00
|
|
|
( -- * Type-safe manipulation tools
|
|
|
|
--
|
|
|
|
-- Types and functions that make handling URIs and JSON-LD contexts less
|
|
|
|
-- error-prone and safer by recording safety checks in the type and
|
|
|
|
-- placing the checks in a single clear place.
|
|
|
|
ActivityPub (..)
|
|
|
|
, Doc (..)
|
|
|
|
|
|
|
|
-- * Actor
|
2019-01-22 00:54:57 +09:00
|
|
|
--
|
|
|
|
-- ActivityPub actor document including a public key, with a 'FromJSON'
|
|
|
|
-- instance for fetching and a 'ToJSON' instance for publishing.
|
2019-02-22 08:59:53 +09:00
|
|
|
, ActorType (..)
|
2019-03-11 08:15:42 +09:00
|
|
|
--, Algorithm (..)
|
2019-02-22 08:59:53 +09:00
|
|
|
, Owner (..)
|
2019-01-22 00:54:57 +09:00
|
|
|
, PublicKey (..)
|
2019-10-17 17:37:48 +09:00
|
|
|
, SshKeyAlgorithm (..)
|
|
|
|
, SshPublicKey (..)
|
2022-07-25 01:52:28 +09:00
|
|
|
, ActorLocal (..)
|
|
|
|
, ActorDetail (..)
|
2019-01-22 00:54:57 +09:00
|
|
|
, Actor (..)
|
2019-09-09 09:27:45 +09:00
|
|
|
, Repo (..)
|
2022-07-25 01:52:28 +09:00
|
|
|
, TicketTracker (..)
|
2019-05-18 07:42:01 +09:00
|
|
|
, CollectionType (..)
|
|
|
|
, Collection (..)
|
2019-05-21 08:51:06 +09:00
|
|
|
, CollectionPageType (..)
|
|
|
|
, CollectionPage (..)
|
2019-05-18 07:42:01 +09:00
|
|
|
, Recipient (..)
|
2022-08-28 22:51:43 +09:00
|
|
|
, Resource (..)
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-06-04 06:52:34 +09:00
|
|
|
-- * Content objects
|
2019-02-12 20:53:24 +09:00
|
|
|
, Note (..)
|
2019-07-12 07:18:30 +09:00
|
|
|
, TicketDependency (..)
|
2020-07-14 18:56:13 +09:00
|
|
|
, PatchLocal (..)
|
2020-05-25 18:40:48 +09:00
|
|
|
, Patch (..)
|
2020-08-13 19:26:20 +09:00
|
|
|
, BundleLocal (..)
|
|
|
|
, Bundle (..)
|
2019-06-06 19:25:16 +09:00
|
|
|
, TicketLocal (..)
|
2020-05-25 21:39:25 +09:00
|
|
|
, MergeRequest (..)
|
2019-06-04 06:52:34 +09:00
|
|
|
, Ticket (..)
|
2019-08-06 22:23:11 +09:00
|
|
|
, Author (..)
|
|
|
|
, Hash (..)
|
|
|
|
, Commit (..)
|
2019-08-29 00:31:40 +09:00
|
|
|
, Branch (..)
|
2022-07-26 02:15:22 +09:00
|
|
|
, Role (..)
|
2019-06-04 06:52:34 +09:00
|
|
|
|
|
|
|
-- * Activity
|
2019-03-14 08:37:58 +09:00
|
|
|
, Accept (..)
|
2020-09-10 19:57:02 +09:00
|
|
|
, AddObject (..)
|
|
|
|
, Add (..)
|
2022-06-23 18:09:02 +09:00
|
|
|
, Apply (..)
|
2020-02-10 23:51:32 +09:00
|
|
|
, CreateObject (..)
|
2019-02-12 20:53:24 +09:00
|
|
|
, Create (..)
|
2019-03-14 08:37:58 +09:00
|
|
|
, Follow (..)
|
2022-07-26 02:15:22 +09:00
|
|
|
, Grant (..)
|
2022-09-06 01:19:52 +09:00
|
|
|
, Invite (..)
|
2020-06-18 19:38:04 +09:00
|
|
|
, OfferObject (..)
|
2019-06-06 23:16:48 +09:00
|
|
|
, Offer (..)
|
2019-08-29 00:31:40 +09:00
|
|
|
, Push (..)
|
2019-03-14 08:37:58 +09:00
|
|
|
, Reject (..)
|
2020-07-23 23:27:11 +09:00
|
|
|
, Resolve (..)
|
2019-09-25 19:43:05 +09:00
|
|
|
, Undo (..)
|
2019-03-14 11:30:36 +09:00
|
|
|
, Audience (..)
|
2019-03-14 08:37:58 +09:00
|
|
|
, SpecificActivity (..)
|
2019-01-22 00:54:57 +09:00
|
|
|
, Activity (..)
|
|
|
|
|
|
|
|
-- * Utilities
|
2020-05-02 02:48:01 +09:00
|
|
|
, emptyAudience
|
2020-02-03 23:53:12 +09:00
|
|
|
, emptyActivity
|
2019-02-07 19:34:33 +09:00
|
|
|
, hActivityPubActor
|
2019-01-22 00:54:57 +09:00
|
|
|
, provideAP
|
2019-06-29 08:15:08 +09:00
|
|
|
, provideAP'
|
2019-01-22 00:54:57 +09:00
|
|
|
, APGetError (..)
|
|
|
|
, httpGetAP
|
2019-03-05 17:26:41 +09:00
|
|
|
, APPostError (..)
|
2019-05-02 08:13:22 +09:00
|
|
|
, hActivityPubForwarder
|
|
|
|
, hForwardingSignature
|
|
|
|
, hForwardedSignature
|
2019-01-22 00:54:57 +09:00
|
|
|
, httpPostAP
|
2019-05-04 06:04:53 +09:00
|
|
|
, httpPostAPBytes
|
2019-02-06 11:48:23 +09:00
|
|
|
, Fetched (..)
|
2020-06-18 19:38:04 +09:00
|
|
|
, fetchAP
|
2019-02-22 08:59:53 +09:00
|
|
|
, fetchAPID
|
2019-04-16 23:27:50 +09:00
|
|
|
, fetchAPID'
|
2022-09-21 21:50:26 +09:00
|
|
|
, fetchTip
|
2019-05-18 07:42:01 +09:00
|
|
|
, fetchRecipient
|
2022-08-28 22:51:43 +09:00
|
|
|
, fetchResource
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 18:20:13 +09:00
|
|
|
, keyListedByActor
|
2019-02-24 02:17:52 +09:00
|
|
|
, fetchUnknownKey
|
|
|
|
, fetchKnownPersonalKey
|
|
|
|
, fetchKnownSharedKey
|
2020-06-18 19:38:04 +09:00
|
|
|
|
|
|
|
, Obj (..)
|
2019-01-22 00:54:57 +09:00
|
|
|
)
|
|
|
|
where
|
|
|
|
|
2019-02-04 08:39:56 +09:00
|
|
|
import Control.Applicative ((<|>), optional)
|
|
|
|
import Control.Exception (Exception, displayException, try)
|
2019-07-23 22:59:48 +09:00
|
|
|
import Control.Monad
|
2019-01-22 00:54:57 +09:00
|
|
|
import Control.Monad.IO.Class
|
2019-02-04 08:39:56 +09:00
|
|
|
import Control.Monad.Trans.Except
|
2019-01-22 00:54:57 +09:00
|
|
|
import Control.Monad.Trans.Writer (Writer)
|
2019-04-26 00:49:15 +09:00
|
|
|
import Crypto.Hash hiding (Context)
|
2019-01-22 00:54:57 +09:00
|
|
|
import Data.Aeson
|
2019-02-22 08:59:53 +09:00
|
|
|
import Data.Aeson.Encoding (pair)
|
|
|
|
import Data.Aeson.Types (Parser, typeMismatch, listEncoding)
|
|
|
|
import Data.Bifunctor
|
2020-09-10 19:57:02 +09:00
|
|
|
import Data.Bitraversable
|
2019-01-22 00:54:57 +09:00
|
|
|
import Data.ByteString (ByteString)
|
2019-08-06 22:23:11 +09:00
|
|
|
import Data.Char
|
2019-02-17 09:14:05 +09:00
|
|
|
import Data.Foldable (for_)
|
2019-07-12 07:18:30 +09:00
|
|
|
import Data.List
|
2019-07-23 22:59:48 +09:00
|
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
2019-02-22 08:59:53 +09:00
|
|
|
import Data.Proxy
|
2019-02-24 10:21:42 +09:00
|
|
|
import Data.Semigroup (Endo, First (..))
|
2019-01-22 00:54:57 +09:00
|
|
|
import Data.Text (Text)
|
2019-10-17 17:37:48 +09:00
|
|
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8, decodeUtf8')
|
2019-02-05 13:05:44 +09:00
|
|
|
import Data.Time.Clock (UTCTime)
|
2019-03-23 11:05:30 +09:00
|
|
|
import Data.Traversable
|
2019-02-22 08:59:53 +09:00
|
|
|
import Network.HTTP.Client hiding (Proxy, proxy)
|
2019-01-22 00:54:57 +09:00
|
|
|
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
|
|
|
|
import Network.HTTP.Simple (JSONException)
|
|
|
|
import Network.HTTP.Types.Header (HeaderName, hContentType)
|
2019-08-06 22:23:11 +09:00
|
|
|
import Text.Email.Parser (EmailAddress)
|
2019-06-02 23:41:51 +09:00
|
|
|
import Text.HTML.SanitizeXSS
|
2019-01-22 00:54:57 +09:00
|
|
|
import Yesod.Core.Content (ContentType)
|
|
|
|
import Yesod.Core.Handler (ProvidedRep, provideRepType)
|
|
|
|
|
2019-04-28 19:18:50 +09:00
|
|
|
import Network.HTTP.Client.Signature
|
|
|
|
|
2019-08-06 22:23:11 +09:00
|
|
|
import qualified Data.Attoparsec.ByteString as A
|
2019-10-17 17:37:48 +09:00
|
|
|
import qualified Data.ByteString.Base64 as B64
|
2019-03-11 08:15:42 +09:00
|
|
|
import qualified Data.ByteString.Char8 as BC
|
2019-05-04 06:04:53 +09:00
|
|
|
import qualified Data.ByteString.Lazy as BL
|
2019-06-06 19:25:16 +09:00
|
|
|
import qualified Data.HashMap.Strict as M
|
2019-08-29 00:31:40 +09:00
|
|
|
import qualified Data.List.NonEmpty as NE
|
2019-07-23 22:59:48 +09:00
|
|
|
import qualified Data.Text as T
|
2019-03-14 11:30:36 +09:00
|
|
|
import qualified Data.Vector as V
|
2019-03-11 08:15:42 +09:00
|
|
|
import qualified Network.HTTP.Signature as S
|
2019-08-06 22:23:11 +09:00
|
|
|
import qualified Text.Email.Parser as E
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-03-11 08:15:42 +09:00
|
|
|
import Crypto.PublicVerifKey
|
2020-08-15 06:16:33 +09:00
|
|
|
import Development.PatchMediaType
|
|
|
|
import Development.PatchMediaType.JSON
|
2019-02-08 08:08:28 +09:00
|
|
|
import Network.FedURI
|
2019-04-26 00:49:15 +09:00
|
|
|
import Network.HTTP.Digest
|
2022-09-21 21:50:26 +09:00
|
|
|
import Web.Text
|
2019-02-08 08:08:28 +09:00
|
|
|
|
2019-02-03 20:01:36 +09:00
|
|
|
import Data.Aeson.Local
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
proxy :: a u -> Proxy a
|
2019-02-22 08:59:53 +09:00
|
|
|
proxy _ = Proxy
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
as2Context :: Text
|
|
|
|
as2Context = "https://www.w3.org/ns/activitystreams"
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
secContext :: Text
|
2022-06-23 18:09:02 +09:00
|
|
|
secContext = "https://w3id.org/security/v2"
|
2019-06-12 09:11:24 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
forgeContext :: Text
|
2022-06-23 18:09:02 +09:00
|
|
|
forgeContext = "https://forgefed.org/ns"
|
2019-06-12 09:11:24 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
publicURI :: Text
|
|
|
|
publicURI = "https://www.w3.org/ns/activitystreams#Public"
|
2019-03-22 04:13:36 +09:00
|
|
|
|
2019-02-22 08:59:53 +09:00
|
|
|
class ActivityPub a where
|
2019-07-23 22:59:48 +09:00
|
|
|
jsonldContext :: Proxy a -> [Text]
|
|
|
|
parseObject :: UriMode u => Object -> Parser (Authority u, a u)
|
|
|
|
toSeries :: UriMode u => Authority u -> a u -> Series
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Doc a u = Doc
|
|
|
|
{ docAuthority :: Authority u
|
|
|
|
, docValue :: a u
|
2019-02-22 08:59:53 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
instance (ActivityPub a, UriMode u) => FromJSON (Doc a u) where
|
2019-06-12 09:11:24 +09:00
|
|
|
parseJSON = withObject "Doc" $ \ o -> uncurry Doc <$> parseObject o
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
instance (ActivityPub a, UriMode u) => ToJSON (Doc a u) where
|
2019-02-22 08:59:53 +09:00
|
|
|
toJSON = error "toJSON Doc"
|
|
|
|
toEncoding (Doc h v) =
|
|
|
|
pairs
|
2019-06-12 09:11:24 +09:00
|
|
|
$ context (jsonldContext $ proxy v)
|
2019-02-22 08:59:53 +09:00
|
|
|
<> toSeries h v
|
2019-06-12 09:11:24 +09:00
|
|
|
where
|
|
|
|
context [] = mempty
|
|
|
|
context [t] = "@context" .= t
|
|
|
|
context ts = "@context" .= ts
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2022-08-16 03:22:46 +09:00
|
|
|
data ActorType
|
|
|
|
= ActorTypePerson
|
|
|
|
| ActorTypeRepo
|
|
|
|
| ActorTypeTicketTracker
|
|
|
|
| ActorTypePatchTracker
|
|
|
|
| ActorTypeOther Text
|
2019-06-11 21:19:51 +09:00
|
|
|
deriving Eq
|
2019-01-22 00:54:57 +09:00
|
|
|
|
|
|
|
instance FromJSON ActorType where
|
2019-03-20 19:36:00 +09:00
|
|
|
parseJSON = withText "ActorType" $ pure . parse
|
|
|
|
where
|
|
|
|
parse t
|
2022-07-25 01:52:28 +09:00
|
|
|
| t == "Person" = ActorTypePerson
|
|
|
|
| t == "Repository" = ActorTypeRepo
|
|
|
|
| t == "TicketTracker" = ActorTypeTicketTracker
|
2022-08-16 03:22:46 +09:00
|
|
|
| t == "PatchTracker" = ActorTypePatchTracker
|
2022-07-25 01:52:28 +09:00
|
|
|
| otherwise = ActorTypeOther t
|
2019-01-22 00:54:57 +09:00
|
|
|
|
|
|
|
instance ToJSON ActorType where
|
|
|
|
toJSON = error "toJSON ActorType"
|
|
|
|
toEncoding at =
|
|
|
|
toEncoding $ case at of
|
2022-07-25 01:52:28 +09:00
|
|
|
ActorTypePerson -> "Person"
|
|
|
|
ActorTypeRepo -> "Repository"
|
|
|
|
ActorTypeTicketTracker -> "TicketTracker"
|
2022-08-16 03:22:46 +09:00
|
|
|
ActorTypePatchTracker -> "PatchTracker"
|
2022-07-25 01:52:28 +09:00
|
|
|
ActorTypeOther t -> t
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-02-22 08:59:53 +09:00
|
|
|
data Owner = OwnerInstance | OwnerActor LocalURI
|
|
|
|
|
|
|
|
ownerShared :: Owner -> Bool
|
|
|
|
ownerShared OwnerInstance = True
|
|
|
|
ownerShared (OwnerActor _) = False
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data PublicKey u = PublicKey
|
|
|
|
{ publicKeyId :: LocalRefURI
|
2019-03-11 08:15:42 +09:00
|
|
|
, publicKeyExpires :: Maybe UTCTime
|
|
|
|
, publicKeyOwner :: Owner
|
|
|
|
, publicKeyMaterial :: PublicVerifKey
|
2019-01-22 00:54:57 +09:00
|
|
|
}
|
|
|
|
|
2019-02-22 08:59:53 +09:00
|
|
|
instance ActivityPub PublicKey where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [secContext]
|
2019-02-22 08:59:53 +09:00
|
|
|
parseObject o = do
|
2019-02-04 08:39:56 +09:00
|
|
|
mtyp <- optional $ o .: "@type" <|> o .: "type"
|
2019-02-22 08:59:53 +09:00
|
|
|
for_ mtyp $ \ t ->
|
2020-05-25 18:37:15 +09:00
|
|
|
unless (t == ("Key" :: Text) || t == "CryptographicKey") $
|
|
|
|
fail "PublicKey @type isn't Key or CryptographicKey"
|
2019-07-23 22:59:48 +09:00
|
|
|
RefURI authority id_ <- o .: "@id" <|> o .: "id"
|
2019-06-12 09:11:24 +09:00
|
|
|
shared <- o .:|? "isShared" .!= False
|
2019-07-23 22:59:48 +09:00
|
|
|
fmap (authority,) $
|
2019-02-22 08:59:53 +09:00
|
|
|
PublicKey id_
|
|
|
|
<$> o .:? "expires"
|
2019-07-23 22:59:48 +09:00
|
|
|
<*> (mkOwner shared =<< withAuthorityO authority (o .: "owner"))
|
2019-03-11 08:15:42 +09:00
|
|
|
<*> (either fail return . decodePublicVerifKeyPEM =<<
|
|
|
|
o .: "publicKeyPem"
|
|
|
|
)
|
2019-01-22 00:54:57 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
mkOwner True lu
|
|
|
|
| lu == topLocalURI = return OwnerInstance
|
|
|
|
mkOwner True _ = fail "Shared key but owner isn't instance URI"
|
|
|
|
mkOwner False lu = return $ OwnerActor lu
|
|
|
|
toSeries authority (PublicKey id_ mexpires owner mat)
|
|
|
|
= "@id" .= RefURI authority id_
|
2019-06-12 09:11:24 +09:00
|
|
|
<> "expires" .=? mexpires
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "owner" .= mkOwner authority owner
|
2019-06-12 09:11:24 +09:00
|
|
|
<> "publicKeyPem" .= encodePublicVerifKeyPEM mat
|
|
|
|
<> "isShared" .= ownerShared owner
|
2019-02-22 08:59:53 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
mkOwner a OwnerInstance = ObjURI a topLocalURI
|
|
|
|
mkOwner a (OwnerActor lu) = ObjURI a lu
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parsePublicKeySet
|
|
|
|
:: UriMode u
|
|
|
|
=> Value
|
|
|
|
-> Parser (Authority u, [Either LocalURI (PublicKey u)])
|
2019-02-22 08:59:53 +09:00
|
|
|
parsePublicKeySet v =
|
|
|
|
case v of
|
|
|
|
Array a ->
|
2019-03-20 18:31:08 +09:00
|
|
|
case V.toList a of
|
|
|
|
[] -> fail "No public keys"
|
|
|
|
k : ks -> do
|
2019-07-23 22:59:48 +09:00
|
|
|
(a, e) <- parseKey k
|
|
|
|
es <- traverse (withAuthorityT a . parseKey) ks
|
|
|
|
return (a, e : es)
|
2019-03-20 18:31:08 +09:00
|
|
|
_ -> second (: []) <$> parseKey v
|
2019-02-22 08:59:53 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
parseKey v@(String _) = second Left . f2l <$> parseJSON v
|
|
|
|
where
|
|
|
|
f2l (ObjURI a l) = (a, l)
|
|
|
|
parseKey (Object o) = second Right <$> parseObject o
|
|
|
|
parseKey v = typeMismatch "PublicKeySet Item" v
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodePublicKeySet
|
|
|
|
:: UriMode u => Authority u -> [Either LocalURI (PublicKey u)] -> Encoding
|
|
|
|
encodePublicKeySet authority es =
|
2019-03-20 18:31:08 +09:00
|
|
|
case es of
|
|
|
|
[e] -> renderKey e
|
|
|
|
_ -> listEncoding renderKey es
|
2019-02-22 08:59:53 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
renderKey (Left lu) = toEncoding $ ObjURI authority lu
|
|
|
|
renderKey (Right pk) = pairs $ toSeries authority pk
|
Support remote actors specifying 2 keys, and DB storage of these keys
It's now possible for activities we be attributed to actors that have more than
one key. We allow up to 2 keys. We also store in the DB. Scaling to support any
number of keys is trivial, but I'm limiting to 2 to avoid potential trouble and
because 2 is the actual number we need.
By having 2 keys, and replacing only one of them in each rotation, we avoid
race conditions. With 1 key, the following can happen:
1. We send an activity to another server
2. We rotate our key
3. The server reaches the activity in its processing queue, tries to verify our
request signature, but fails because it can't fetch the key. It's the old
key and we discarded it already, replaced it with the new one
When we use 2 keys, the previous key remains available and other servers have
time to finish processing our requests signed with that key. We can safely
rotate, without worrying about whether the user sent anything right before the
rotation time.
Caveat: With this feature, we allow OTHER servers to rotate freely. It's safe
because it's optional, but it's just Vervis right now. Once Vervis itself
starts using 2 keys, it will be able to rotate freely without race condition
risk, but probably Mastodon etc. won't accept its signatures because of the use
of 2 keys and because they're server-scope keys.
Maybe I can get these features adopted by the fediverse?
2019-02-05 04:38:50 +09:00
|
|
|
|
2019-10-17 17:37:48 +09:00
|
|
|
data SshKeyAlgorithm
|
|
|
|
= SshKeyAlgorithmRSA
|
|
|
|
| SshKeyAlgorithmDSA
|
|
|
|
| SshKeyAlgorithmECDSA
|
|
|
|
| SshKeyAlgorithmEd25519
|
|
|
|
|
|
|
|
instance FromJSON SshKeyAlgorithm where
|
|
|
|
parseJSON = withText "SshKeyAlgorithm" parse
|
|
|
|
where
|
|
|
|
parse t
|
|
|
|
| t == "ssh-rsa" = pure SshKeyAlgorithmRSA
|
|
|
|
| t == "ssh-dsa" = pure SshKeyAlgorithmDSA
|
|
|
|
| t == "ssh-ecdsa" = pure SshKeyAlgorithmECDSA
|
|
|
|
| t == "ssh-ed25519" = pure SshKeyAlgorithmEd25519
|
|
|
|
| otherwise =
|
|
|
|
fail $ "Unrecognized ssh key algo: " ++ T.unpack t
|
|
|
|
|
|
|
|
instance ToJSON SshKeyAlgorithm where
|
|
|
|
toJSON = error "toJSON SshKeyAlgorithm"
|
|
|
|
toEncoding = toEncoding . render
|
|
|
|
where
|
|
|
|
render :: SshKeyAlgorithm -> Text
|
|
|
|
render SshKeyAlgorithmRSA = "ssh-rsa"
|
|
|
|
render SshKeyAlgorithmDSA = "ssh-dsa"
|
|
|
|
render SshKeyAlgorithmECDSA = "ssh-ecdsa"
|
|
|
|
render SshKeyAlgorithmEd25519 = "ssh-ed25519"
|
|
|
|
|
|
|
|
data SshPublicKey u = SshPublicKey
|
|
|
|
{ sshPublicKeyId :: LocalURI
|
|
|
|
, sshPublicKeyExpires :: Maybe UTCTime
|
|
|
|
, sshPublicKeyOwner :: LocalURI
|
|
|
|
, sshPublicKeyAlgorithm :: SshKeyAlgorithm
|
|
|
|
, sshPublicKeyMaterial :: ByteString
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub SshPublicKey where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [secContext, forgeContext]
|
2019-10-17 17:37:48 +09:00
|
|
|
parseObject o = do
|
|
|
|
mtyp <- optional $ o .: "@type" <|> o .: "type"
|
|
|
|
for_ mtyp $ \ t ->
|
|
|
|
when (t /= ("SshKey" :: Text)) $
|
|
|
|
fail "SshKey @type isn't SshKey"
|
|
|
|
|
|
|
|
mediaType <- o .: "mediaType"
|
|
|
|
unless (mediaType == ("application/octet-stream" :: Text)) $
|
|
|
|
fail "mediaType isn't octet-stream"
|
|
|
|
|
|
|
|
ObjURI authority luId <- o .: "@id" <|> o .: "id"
|
|
|
|
fmap (authority,) $
|
|
|
|
SshPublicKey luId
|
|
|
|
<$> o .:? "expires"
|
|
|
|
<*> withAuthorityO authority (o .: "owner")
|
|
|
|
<*> o .: "sshKeyType"
|
|
|
|
<*> (decodeBase64 . encodeUtf8 =<< o .: "content")
|
|
|
|
where
|
|
|
|
decodeBase64 = either fail return . B64.decode
|
|
|
|
toSeries authority (SshPublicKey luId mexpires owner algo mat)
|
|
|
|
= "@id" .= ObjURI authority luId
|
|
|
|
<> "expires" .=? mexpires
|
|
|
|
<> "owner" .= ObjURI authority owner
|
|
|
|
<> "sshKeyType" .= algo
|
|
|
|
<> "mediaType" .= ("application/octet-stream" :: Text)
|
|
|
|
<> "content" .= decodeUtf8 (B64.encode mat)
|
|
|
|
|
2022-07-25 01:52:28 +09:00
|
|
|
data ActorLocal u = ActorLocal
|
2019-02-22 08:59:53 +09:00
|
|
|
{ actorId :: LocalURI
|
|
|
|
, actorInbox :: LocalURI
|
2019-05-21 09:36:05 +09:00
|
|
|
, actorOutbox :: Maybe LocalURI
|
2019-06-11 21:19:51 +09:00
|
|
|
, actorFollowers :: Maybe LocalURI
|
2019-10-19 17:15:48 +09:00
|
|
|
, actorFollowing :: Maybe LocalURI
|
2019-07-23 22:59:48 +09:00
|
|
|
, actorPublicKeys :: [Either LocalURI (PublicKey u)]
|
2019-10-17 17:37:48 +09:00
|
|
|
, actorSshKeys :: [LocalURI]
|
2019-01-22 00:54:57 +09:00
|
|
|
}
|
|
|
|
|
2022-07-25 01:52:28 +09:00
|
|
|
parseActorLocal :: UriMode u => Object -> Parser (Maybe (Authority u, ActorLocal u))
|
|
|
|
parseActorLocal o = do
|
|
|
|
mid <- o .:? "id"
|
|
|
|
case mid of
|
|
|
|
Nothing -> do
|
|
|
|
verifyNothing "inbox"
|
|
|
|
verifyNothing "outbox"
|
|
|
|
verifyNothing "followers"
|
|
|
|
verifyNothing "following"
|
|
|
|
verifyNothing "publicKey"
|
|
|
|
verifyNothing "sshKey"
|
|
|
|
return Nothing
|
|
|
|
Just (ObjURI a id_) ->
|
|
|
|
fmap (Just . (a,)) $
|
|
|
|
ActorLocal
|
|
|
|
<$> pure id_
|
|
|
|
<*> withAuthorityO a (o .: "inbox")
|
|
|
|
<*> withAuthorityMaybeO a (o .:? "outbox")
|
|
|
|
<*> withAuthorityMaybeO a (o .:? "followers")
|
|
|
|
<*> withAuthorityMaybeO a (o .:? "following")
|
|
|
|
<*> withAuthorityT a (parsePublicKeySet =<< o .: "publicKey")
|
|
|
|
<*> (traverse (withAuthorityO a . return) =<< o .:? "sshKey" .!= [])
|
|
|
|
where
|
|
|
|
verifyNothing t =
|
|
|
|
if t `M.member` o
|
|
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
|
|
else return ()
|
|
|
|
|
|
|
|
encodeActorLocal :: UriMode u => Authority u -> ActorLocal u -> Series
|
|
|
|
encodeActorLocal a (ActorLocal id_ inbox outbox followers following pkeys skeys)
|
|
|
|
= "id" .= ObjURI a id_
|
|
|
|
<> "inbox" .= ObjURI a inbox
|
|
|
|
<> "outbox" .=? (ObjURI a <$> outbox)
|
|
|
|
<> "followers" .=? (ObjURI a <$> followers)
|
|
|
|
<> "following" .=? (ObjURI a <$> following)
|
|
|
|
<> "publicKey" `pair` encodePublicKeySet a pkeys
|
|
|
|
<> "sshKey" .=% map (ObjURI a) skeys
|
|
|
|
|
|
|
|
data ActorDetail = ActorDetail
|
|
|
|
{ actorType :: ActorType
|
|
|
|
, actorUsername :: Maybe Text
|
|
|
|
, actorName :: Maybe Text
|
|
|
|
, actorSummary :: Maybe Text
|
|
|
|
}
|
|
|
|
|
|
|
|
parseActorDetail :: Object -> Parser ActorDetail
|
|
|
|
parseActorDetail o =
|
|
|
|
ActorDetail
|
|
|
|
<$> o .: "type"
|
|
|
|
<*> o .:? "preferredUsername"
|
|
|
|
<*> o .:? "name"
|
|
|
|
<*> o .:? "summary"
|
|
|
|
|
|
|
|
encodeActorDetail :: ActorDetail -> Series
|
|
|
|
encodeActorDetail (ActorDetail typ musername mname msummary)
|
|
|
|
= "type" .= typ
|
|
|
|
<> "preferredUsername" .=? musername
|
|
|
|
<> "name" .=? mname
|
|
|
|
<> "summary" .=? msummary
|
|
|
|
|
|
|
|
data Actor u = Actor
|
|
|
|
{ actorLocal :: ActorLocal u
|
|
|
|
, actorDetail :: ActorDetail
|
|
|
|
}
|
|
|
|
|
2019-02-22 08:59:53 +09:00
|
|
|
instance ActivityPub Actor where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, secContext, forgeContext]
|
2019-02-22 08:59:53 +09:00
|
|
|
parseObject o = do
|
2022-07-25 01:52:28 +09:00
|
|
|
mlocal <- parseActorLocal o
|
|
|
|
(h, local) <-
|
|
|
|
case mlocal of
|
|
|
|
Nothing -> fail "No ActorLocal"
|
|
|
|
Just l -> return l
|
|
|
|
detail <- parseActorDetail o
|
|
|
|
return (h, Actor local detail)
|
|
|
|
toSeries h (Actor local detail) =
|
|
|
|
encodeActorLocal h local <> encodeActorDetail detail
|
2019-06-11 21:19:51 +09:00
|
|
|
|
2019-09-09 09:27:45 +09:00
|
|
|
data Repo u = Repo
|
|
|
|
{ repoActor :: Actor u
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
, repoTeam :: Maybe LocalURI
|
2020-08-15 06:16:33 +09:00
|
|
|
, repoVcs :: VersionControlSystem
|
2022-09-17 17:31:22 +09:00
|
|
|
, repoLoom :: Maybe LocalURI
|
2019-09-09 09:27:45 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub Repo where
|
2020-08-15 06:16:33 +09:00
|
|
|
jsonldContext _ = [as2Context, secContext, forgeContext]
|
2019-09-09 09:27:45 +09:00
|
|
|
parseObject o = do
|
|
|
|
(h, a) <- parseObject o
|
2022-07-25 01:52:28 +09:00
|
|
|
unless (actorType (actorDetail a) == ActorTypeRepo) $
|
2019-09-09 09:27:45 +09:00
|
|
|
fail "Actor type isn't Repository"
|
|
|
|
fmap (h,) $
|
|
|
|
Repo a
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<$> withAuthorityMaybeO h (o .:|? "team")
|
2020-08-15 06:16:33 +09:00
|
|
|
<*> o .: "versionControlSystem"
|
2022-09-17 17:31:22 +09:00
|
|
|
<*> withAuthorityMaybeO h (o .:? "sendPatchesTo")
|
|
|
|
toSeries authority (Repo actor team vcs loom)
|
2019-09-09 09:27:45 +09:00
|
|
|
= toSeries authority actor
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<> "team" .= (ObjURI authority <$> team)
|
2020-08-15 06:16:33 +09:00
|
|
|
<> "versionControlSystem" .= vcs
|
2022-09-17 17:31:22 +09:00
|
|
|
<> "sendPatchesTo" .=? (ObjURI authority <$> loom)
|
2019-09-09 09:27:45 +09:00
|
|
|
|
2022-07-25 01:52:28 +09:00
|
|
|
data TicketTracker u = TicketTracker
|
|
|
|
{ ticketTrackerActor :: Actor u
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
, ticketTrackerTeam :: Maybe LocalURI
|
2019-06-11 21:19:51 +09:00
|
|
|
}
|
|
|
|
|
2022-07-25 18:10:24 +09:00
|
|
|
instance ActivityPub TicketTracker where
|
|
|
|
jsonldContext _ = [as2Context, secContext, forgeContext]
|
2019-06-11 21:19:51 +09:00
|
|
|
parseObject o = do
|
|
|
|
(h, a) <- parseObject o
|
2022-07-25 01:52:28 +09:00
|
|
|
unless (actorType (actorDetail a) == ActorTypeTicketTracker) $
|
|
|
|
fail "Actor type isn't TicketTracker"
|
2019-06-11 21:19:51 +09:00
|
|
|
fmap (h,) $
|
2022-07-25 01:52:28 +09:00
|
|
|
TicketTracker a
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<$> withAuthorityMaybeO h (o .:|? "team")
|
2022-07-25 01:52:28 +09:00
|
|
|
toSeries authority (TicketTracker actor team)
|
2019-07-23 22:59:48 +09:00
|
|
|
= toSeries authority actor
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<> "team" .= (ObjURI authority <$> team)
|
2019-03-10 15:42:03 +09:00
|
|
|
|
2019-05-18 07:42:01 +09:00
|
|
|
data CollectionType = CollectionTypeUnordered | CollectionTypeOrdered
|
|
|
|
|
|
|
|
instance FromJSON CollectionType where
|
|
|
|
parseJSON = withText "CollectionType" parse
|
|
|
|
where
|
|
|
|
parse "Collection" = pure CollectionTypeUnordered
|
|
|
|
parse "OrderedCollection" = pure CollectionTypeOrdered
|
|
|
|
parse t = fail $ "Unknown collection type: " ++ T.unpack t
|
|
|
|
|
|
|
|
instance ToJSON CollectionType where
|
|
|
|
toJSON = error "toJSON CollectionType"
|
|
|
|
toEncoding ct =
|
|
|
|
toEncoding $ case ct of
|
|
|
|
CollectionTypeUnordered -> "Collection" :: Text
|
|
|
|
CollectionTypeOrdered -> "OrderedCollection"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Collection a u = Collection
|
2019-05-18 07:42:01 +09:00
|
|
|
{ collectionId :: LocalURI
|
|
|
|
, collectionType :: CollectionType
|
|
|
|
, collectionTotalItems :: Maybe Int
|
|
|
|
, collectionCurrent :: Maybe LocalURI
|
2019-05-21 08:51:06 +09:00
|
|
|
, collectionFirst :: Maybe LocalPageURI
|
|
|
|
, collectionLast :: Maybe LocalPageURI
|
2019-05-18 07:42:01 +09:00
|
|
|
, collectionItems :: [a]
|
|
|
|
}
|
|
|
|
|
|
|
|
instance (FromJSON a, ToJSON a) => ActivityPub (Collection a) where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
2019-05-18 07:42:01 +09:00
|
|
|
parseObject o = do
|
2019-07-23 22:59:48 +09:00
|
|
|
ObjURI authority id_ <- o .: "id"
|
|
|
|
fmap (authority,) $
|
2019-05-18 07:42:01 +09:00
|
|
|
Collection id_
|
|
|
|
<$> o .: "type"
|
|
|
|
<*> o .:? "totalItems"
|
2019-07-23 22:59:48 +09:00
|
|
|
<*> withAuthorityMaybeO authority (o .:? "current")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "first")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "last")
|
2019-05-18 07:42:01 +09:00
|
|
|
<*> optional (o .: "items" <|> o .: "orderedItems") .!= []
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries authority (Collection id_ typ total curr firzt last items)
|
|
|
|
= "id" .= ObjURI authority id_
|
2019-05-18 07:42:01 +09:00
|
|
|
<> "type" .= typ
|
|
|
|
<> "totalItems" .=? total
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "current" .=? (ObjURI authority <$> curr)
|
|
|
|
<> "first" .=? (PageURI authority <$> firzt)
|
|
|
|
<> "last" .=? (PageURI authority <$> last)
|
2019-11-07 05:46:21 +09:00
|
|
|
<> itemsProp .=% items
|
|
|
|
where
|
|
|
|
itemsProp =
|
|
|
|
case typ of
|
|
|
|
CollectionTypeUnordered -> "items"
|
|
|
|
CollectionTypeOrdered -> "orderedItems"
|
2019-05-21 08:51:06 +09:00
|
|
|
|
|
|
|
data CollectionPageType
|
|
|
|
= CollectionPageTypeUnordered
|
|
|
|
| CollectionPageTypeOrdered
|
|
|
|
|
|
|
|
instance FromJSON CollectionPageType where
|
|
|
|
parseJSON = withText "CollectionPageType" parse
|
|
|
|
where
|
|
|
|
parse "CollectionPage" = pure CollectionPageTypeUnordered
|
|
|
|
parse "OrderedCollectionPage" = pure CollectionPageTypeOrdered
|
|
|
|
parse t = fail $ "Unknown collection page type: " ++ T.unpack t
|
|
|
|
|
|
|
|
instance ToJSON CollectionPageType where
|
|
|
|
toJSON = error "toJSON CollectionPageType"
|
|
|
|
toEncoding ct =
|
|
|
|
toEncoding $ case ct of
|
|
|
|
CollectionPageTypeUnordered -> "CollectionPage" :: Text
|
|
|
|
CollectionPageTypeOrdered -> "OrderedCollectionPage"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data CollectionPage a u = CollectionPage
|
2019-05-21 08:51:06 +09:00
|
|
|
{ collectionPageId :: LocalPageURI
|
|
|
|
, collectionPageType :: CollectionPageType
|
|
|
|
, collectionPageTotalItems :: Maybe Int
|
|
|
|
, collectionPageCurrent :: Maybe LocalPageURI
|
|
|
|
, collectionPageFirst :: Maybe LocalPageURI
|
|
|
|
, collectionPageLast :: Maybe LocalPageURI
|
|
|
|
, collectionPagePartOf :: LocalURI
|
|
|
|
, collectionPagePrev :: Maybe LocalPageURI
|
|
|
|
, collectionPageNext :: Maybe LocalPageURI
|
|
|
|
, collectionPageStartIndex :: Maybe Int
|
|
|
|
, collectionPageItems :: [a]
|
|
|
|
}
|
|
|
|
|
|
|
|
instance (FromJSON a, ToJSON a) => ActivityPub (CollectionPage a) where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
2019-05-21 08:51:06 +09:00
|
|
|
parseObject o = do
|
2019-07-23 22:59:48 +09:00
|
|
|
PageURI authority id_ <- o .: "id"
|
|
|
|
fmap (authority,) $
|
2019-05-21 08:51:06 +09:00
|
|
|
CollectionPage id_
|
|
|
|
<$> o .: "type"
|
|
|
|
<*> o .:? "totalItems"
|
2019-07-23 22:59:48 +09:00
|
|
|
<*> withAuthorityMaybeP authority (o .:? "current")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "first")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "last")
|
|
|
|
<*> withAuthorityO authority (o .: "partOf")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "prev")
|
|
|
|
<*> withAuthorityMaybeP authority (o .:? "next")
|
2019-05-21 08:51:06 +09:00
|
|
|
<*> o .:? "startIndex"
|
|
|
|
<*> optional (o .: "items" <|> o .: "orderedItems") .!= []
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries authority (CollectionPage id_ typ total curr firzt last partOf prev next ind items)
|
|
|
|
= "id" .= PageURI authority id_
|
2019-05-21 08:51:06 +09:00
|
|
|
<> "type" .= typ
|
|
|
|
<> "totalItems" .=? total
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "current" .=? (PageURI authority <$> curr)
|
|
|
|
<> "first" .=? (PageURI authority <$> firzt)
|
|
|
|
<> "last" .=? (PageURI authority <$> last)
|
|
|
|
<> "partOf" .= (ObjURI authority partOf)
|
|
|
|
<> "prev" .=? (PageURI authority <$> prev)
|
|
|
|
<> "next" .=? (PageURI authority <$> next)
|
2019-05-21 08:51:06 +09:00
|
|
|
<> "startIndex" .=? ind
|
2019-11-07 05:46:21 +09:00
|
|
|
<> itemsProp .=% items
|
|
|
|
where
|
|
|
|
itemsProp =
|
|
|
|
case typ of
|
|
|
|
CollectionPageTypeUnordered -> "items"
|
|
|
|
CollectionPageTypeOrdered -> "orderedItems"
|
2019-05-18 07:42:01 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Recipient u = RecipientActor (Actor u) | RecipientCollection (Collection (ObjURI u) u)
|
2019-05-18 07:42:01 +09:00
|
|
|
|
|
|
|
instance ActivityPub Recipient where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, secContext, forgeContext]
|
2019-05-18 07:42:01 +09:00
|
|
|
parseObject o =
|
|
|
|
second RecipientActor <$> parseObject o <|>
|
|
|
|
second RecipientCollection <$> parseObject o
|
|
|
|
toSeries h (RecipientActor a) = toSeries h a
|
|
|
|
toSeries h (RecipientCollection c) = toSeries h c
|
|
|
|
|
2022-08-28 22:51:43 +09:00
|
|
|
data Resource u = ResourceActor (Actor u) | ResourceChild LocalURI LocalURI
|
|
|
|
|
|
|
|
instance ActivityPub Resource where
|
|
|
|
jsonldContext _ = [as2Context, secContext, forgeContext]
|
|
|
|
parseObject o =
|
|
|
|
second ResourceActor <$> parseObject o <|> do
|
|
|
|
ObjURI h luId <- o .: "id" <|> o .: "@id"
|
|
|
|
(h,) . ResourceChild luId <$> withAuthorityO h (o .: "managedBy")
|
|
|
|
toSeries h (ResourceActor a) = toSeries h a
|
|
|
|
toSeries h (ResourceChild luId luManager)
|
|
|
|
= "id" .= ObjURI h luId
|
|
|
|
<> "managedBy" .= ObjURI h luManager
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Audience u = Audience
|
|
|
|
{ audienceTo :: [ObjURI u]
|
|
|
|
, audienceBto :: [ObjURI u]
|
|
|
|
, audienceCc :: [ObjURI u]
|
|
|
|
, audienceBcc :: [ObjURI u]
|
|
|
|
, audienceGeneral :: [ObjURI u]
|
|
|
|
, audienceNonActors :: [ObjURI u]
|
2019-03-23 11:57:34 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
newtype AdaptAudience u = AdaptAudience
|
|
|
|
{ unAdapt :: ObjURI u
|
2019-03-23 11:57:34 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
instance UriMode u => FromJSON (AdaptAudience u) where
|
2019-05-17 06:22:45 +09:00
|
|
|
parseJSON = fmap AdaptAudience . parseJSON . adapt
|
2019-03-23 11:57:34 +09:00
|
|
|
where
|
|
|
|
adapt v =
|
|
|
|
case v of
|
|
|
|
String t
|
2019-07-23 22:59:48 +09:00
|
|
|
| t == "as:Public" -> String "Public"
|
|
|
|
| t == publicURI -> String "Public"
|
2019-03-23 11:57:34 +09:00
|
|
|
_ -> v
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseAudience :: UriMode u => Object -> Parser (Audience u)
|
2019-03-23 11:57:34 +09:00
|
|
|
parseAudience o =
|
|
|
|
Audience
|
2019-04-02 08:40:29 +09:00
|
|
|
<$> o .:& "to"
|
|
|
|
<*> o .:& "bto"
|
|
|
|
<*> o .:& "cc"
|
|
|
|
<*> o .:& "bcc"
|
|
|
|
<*> o .:& "audience"
|
2019-06-12 09:11:24 +09:00
|
|
|
<*> o .:|& "nonActors"
|
2019-03-23 11:57:34 +09:00
|
|
|
where
|
|
|
|
obj .:& key = do
|
2019-04-02 08:40:29 +09:00
|
|
|
l <- obj .:? key .!= []
|
|
|
|
return $ map unAdapt l
|
2019-06-12 09:11:24 +09:00
|
|
|
obj .:|& key = do
|
|
|
|
l <- obj .:|? key .!= []
|
|
|
|
return $ map unAdapt l
|
2019-03-23 11:57:34 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeAudience :: UriMode u => Audience u -> Series
|
2019-05-17 19:47:53 +09:00
|
|
|
encodeAudience (Audience to bto cc bcc aud nons)
|
2019-06-12 09:11:24 +09:00
|
|
|
= "to" .=% to
|
|
|
|
<> "bto" .=% bto
|
|
|
|
<> "cc" .=% cc
|
|
|
|
<> "bcc" .=% bcc
|
|
|
|
<> "audience" .=% aud
|
|
|
|
<> "nonActors" .=% nons
|
2019-03-23 11:57:34 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Note u = Note
|
2019-03-23 11:05:30 +09:00
|
|
|
{ noteId :: Maybe LocalURI
|
2019-03-23 05:46:42 +09:00
|
|
|
, noteAttrib :: LocalURI
|
2019-07-23 22:59:48 +09:00
|
|
|
, noteAudience :: Audience u
|
|
|
|
, noteReplyTo :: Maybe (ObjURI u)
|
|
|
|
, noteContext :: Maybe (ObjURI u)
|
2019-03-22 07:57:15 +09:00
|
|
|
, notePublished :: Maybe UTCTime
|
2019-06-02 23:41:51 +09:00
|
|
|
, noteSource :: Text
|
2019-03-22 07:57:15 +09:00
|
|
|
, noteContent :: Text
|
2019-01-22 00:54:57 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
withAuthorityT a m = do
|
|
|
|
(a', v) <- m
|
|
|
|
if a == a'
|
2019-03-23 05:46:42 +09:00
|
|
|
then return v
|
2019-07-23 22:59:48 +09:00
|
|
|
else fail "URI authority mismatch"
|
2019-03-23 05:46:42 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
withAuthorityO a m = do
|
|
|
|
ObjURI a' v <- m
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
|
|
|
withAuthorityS a m = do
|
|
|
|
SubURI a' v <- m
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
|
|
|
withAuthorityP a m = do
|
|
|
|
PageURI a' v <- m
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
2020-09-10 19:57:02 +09:00
|
|
|
withAuthorityD a m = do
|
|
|
|
Doc a' v <- m
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
withAuthorityMaybeT a m = do
|
|
|
|
mu <- m
|
|
|
|
for mu $ \ (a', v) ->
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
|
|
|
withAuthorityMaybeO a m = do
|
|
|
|
mu <- m
|
|
|
|
for mu $ \ (ObjURI a' v) ->
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
|
|
|
withAuthorityMaybeS a m = do
|
|
|
|
mu <- m
|
|
|
|
for mu $ \ (SubURI a' v) ->
|
|
|
|
if a == a'
|
|
|
|
then return v
|
|
|
|
else fail "URI authority mismatch"
|
|
|
|
|
|
|
|
withAuthorityMaybeP a m = do
|
|
|
|
mu <- m
|
|
|
|
for mu $ \ (PageURI a' v) ->
|
|
|
|
if a == a'
|
2019-03-23 11:05:30 +09:00
|
|
|
then return v
|
2019-07-23 22:59:48 +09:00
|
|
|
else fail "URI authority mismatch"
|
2019-03-23 11:05:30 +09:00
|
|
|
|
2019-03-23 05:46:42 +09:00
|
|
|
instance ActivityPub Note where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context]
|
2019-03-23 05:46:42 +09:00
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
2019-06-02 23:41:51 +09:00
|
|
|
unless (typ == ("Note" :: Text)) $
|
|
|
|
fail "type isn't Note"
|
|
|
|
|
|
|
|
mediaType <- o .: "mediaType"
|
|
|
|
unless (mediaType == ("text/html" :: Text)) $
|
|
|
|
fail "mediaType isn't HTML"
|
|
|
|
|
|
|
|
source <- o .: "source"
|
|
|
|
sourceType <- source .: "mediaType"
|
|
|
|
unless (sourceType == ("text/markdown; variant=Pandoc" :: Text)) $
|
|
|
|
fail "source mediaType isn't Pandoc Markdown"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
ObjURI a attrib <- o .: "attributedTo"
|
|
|
|
fmap (a,) $
|
2019-03-23 11:05:30 +09:00
|
|
|
Note
|
2019-07-23 22:59:48 +09:00
|
|
|
<$> withAuthorityMaybeO a (o .:? "id")
|
2019-03-23 11:05:30 +09:00
|
|
|
<*> pure attrib
|
2019-03-23 11:57:34 +09:00
|
|
|
<*> parseAudience o
|
2019-03-23 05:46:42 +09:00
|
|
|
<*> o .:? "inReplyTo"
|
|
|
|
<*> o .:? "context"
|
|
|
|
<*> o .:? "published"
|
2019-06-02 23:41:51 +09:00
|
|
|
<*> source .: "content"
|
|
|
|
<*> (sanitizeBalance <$> o .: "content")
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries authority (Note mid attrib aud mreply mcontext mpublished src content)
|
2019-03-23 05:46:42 +09:00
|
|
|
= "type" .= ("Note" :: Text)
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "id" .=? (ObjURI authority <$> mid)
|
|
|
|
<> "attributedTo" .= ObjURI authority attrib
|
2019-03-23 11:57:34 +09:00
|
|
|
<> encodeAudience aud
|
2019-03-23 05:46:42 +09:00
|
|
|
<> "inReplyTo" .=? mreply
|
|
|
|
<> "context" .=? mcontext
|
|
|
|
<> "published" .=? mpublished
|
2019-06-02 23:41:51 +09:00
|
|
|
<> "source" .= object
|
|
|
|
[ "content" .= src
|
|
|
|
, "mediaType" .= ("text/markdown; variant=Pandoc" :: Text)
|
|
|
|
]
|
2019-03-23 05:46:42 +09:00
|
|
|
<> "content" .= content
|
2019-06-02 23:41:51 +09:00
|
|
|
<> "mediaType" .= ("text/html" :: Text)
|
2019-03-23 05:46:42 +09:00
|
|
|
|
2019-07-12 07:18:30 +09:00
|
|
|
data RelationshipProperty = RelDependsOn deriving Eq
|
2019-07-12 00:14:16 +09:00
|
|
|
|
|
|
|
instance FromJSON RelationshipProperty where
|
|
|
|
parseJSON = withText "RelationshipProperty" parse
|
|
|
|
where
|
|
|
|
parse t
|
|
|
|
| t == "dependsOn" = pure RelDependsOn
|
|
|
|
| otherwise = fail $ "Unrecognized relationship: " ++ T.unpack t
|
|
|
|
|
|
|
|
instance ToJSON RelationshipProperty where
|
|
|
|
toJSON = error "toJSON RelationshipProperty"
|
|
|
|
toEncoding at =
|
|
|
|
toEncoding $ case at of
|
|
|
|
RelDependsOn -> "dependsOn" :: Text
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Relationship u = Relationship
|
|
|
|
{ relationshipId :: Maybe (ObjURI u)
|
2019-07-12 07:18:30 +09:00
|
|
|
, relationshipExtraTypes :: [Text]
|
2019-07-23 22:59:48 +09:00
|
|
|
, relationshipSubject :: ObjURI u
|
2019-07-12 00:14:16 +09:00
|
|
|
, relationshipProperty :: Either RelationshipProperty Text
|
2019-07-23 22:59:48 +09:00
|
|
|
, relationshipObject :: ObjURI u
|
2019-07-12 00:14:16 +09:00
|
|
|
, relationshipAttributedTo :: LocalURI
|
|
|
|
, relationshipPublished :: Maybe UTCTime
|
|
|
|
, relationshipUpdated :: Maybe UTCTime
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub Relationship where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
parseObject o = do
|
2019-07-12 07:18:30 +09:00
|
|
|
typs <- o .: "type"
|
|
|
|
unless (("Relationship" :: Text) `elem` typs) $
|
2019-07-12 00:14:16 +09:00
|
|
|
fail "type isn't Relationship"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
ObjURI a attributedTo <- o .: "attributedTo"
|
2019-07-12 00:14:16 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fmap (a,) $
|
2019-07-12 00:14:16 +09:00
|
|
|
Relationship
|
|
|
|
<$> o .:? "id"
|
2019-07-12 07:18:30 +09:00
|
|
|
<*> pure (delete "Relationship" typs)
|
2019-07-12 00:14:16 +09:00
|
|
|
<*> o .: "subject"
|
|
|
|
<*> o .:+ "relationship"
|
|
|
|
<*> o .: "object"
|
|
|
|
<*> pure attributedTo
|
|
|
|
<*> o .:? "published"
|
|
|
|
<*> o .:? "updated"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries authority
|
2019-07-12 07:18:30 +09:00
|
|
|
(Relationship id_ typs subject property object attributedTo published
|
2020-06-18 19:38:04 +09:00
|
|
|
updated)
|
2019-07-12 00:14:16 +09:00
|
|
|
= "id" .=? id_
|
2019-07-12 07:18:30 +09:00
|
|
|
<> "type" .= ("Relationship" : typs)
|
2019-07-12 00:14:16 +09:00
|
|
|
<> "subject" .= subject
|
|
|
|
<> "relationship" .=+ property
|
|
|
|
<> "object" .= object
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "attributedTo" .= ObjURI authority attributedTo
|
2019-07-12 00:14:16 +09:00
|
|
|
<> "published" .=? published
|
|
|
|
<> "updated" .=? updated
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data TicketDependency u = TicketDependency
|
|
|
|
{ ticketDepId :: Maybe (ObjURI u)
|
|
|
|
, ticketDepParent :: ObjURI u
|
|
|
|
, ticketDepChild :: ObjURI u
|
2019-07-12 07:18:30 +09:00
|
|
|
, ticketDepAttributedTo :: LocalURI
|
|
|
|
, ticketDepPublished :: Maybe UTCTime
|
|
|
|
, ticketDepUpdated :: Maybe UTCTime
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub TicketDependency where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
parseObject o = do
|
2019-07-23 22:59:48 +09:00
|
|
|
(a, rel) <- parseObject o
|
2019-07-12 07:18:30 +09:00
|
|
|
unless ("TicketDependency" `elem` relationshipExtraTypes rel) $
|
|
|
|
fail "type isn't TicketDependency"
|
|
|
|
|
|
|
|
unless (relationshipProperty rel == Left RelDependsOn) $
|
|
|
|
fail "relationship isn't dependsOn"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
return (a, rel2td rel)
|
2019-07-12 07:18:30 +09:00
|
|
|
where
|
|
|
|
rel2td rel = TicketDependency
|
|
|
|
{ ticketDepId = relationshipId rel
|
|
|
|
, ticketDepParent = relationshipSubject rel
|
|
|
|
, ticketDepChild = relationshipObject rel
|
|
|
|
, ticketDepAttributedTo = relationshipAttributedTo rel
|
|
|
|
, ticketDepPublished = relationshipPublished rel
|
|
|
|
, ticketDepUpdated = relationshipUpdated rel
|
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries a = toSeries a . td2rel
|
2019-07-12 07:18:30 +09:00
|
|
|
where
|
|
|
|
td2rel td = Relationship
|
|
|
|
{ relationshipId = ticketDepId td
|
|
|
|
, relationshipExtraTypes = ["TicketDependency"]
|
|
|
|
, relationshipSubject = ticketDepParent td
|
|
|
|
, relationshipProperty = Left RelDependsOn
|
|
|
|
, relationshipObject = ticketDepChild td
|
|
|
|
, relationshipAttributedTo = ticketDepAttributedTo td
|
|
|
|
, relationshipPublished = ticketDepPublished td
|
|
|
|
, relationshipUpdated = ticketDepUpdated td
|
|
|
|
}
|
|
|
|
|
2020-07-14 18:56:13 +09:00
|
|
|
data PatchLocal = PatchLocal
|
2020-08-13 19:26:20 +09:00
|
|
|
{ patchId :: LocalURI
|
|
|
|
, patchContext :: LocalURI
|
2020-07-14 18:56:13 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
parsePatchLocal
|
|
|
|
:: UriMode u => Object -> Parser (Maybe (Authority u, PatchLocal))
|
|
|
|
parsePatchLocal o = do
|
|
|
|
mid <- o .:? "id"
|
|
|
|
case mid of
|
|
|
|
Nothing -> do
|
|
|
|
verifyNothing "context"
|
|
|
|
return Nothing
|
|
|
|
Just (ObjURI a id_) ->
|
|
|
|
fmap (Just . (a,)) $
|
|
|
|
PatchLocal
|
|
|
|
<$> pure id_
|
|
|
|
<*> withAuthorityO a (o .: "context")
|
|
|
|
where
|
|
|
|
verifyNothing t =
|
|
|
|
if t `M.member` o
|
|
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
|
|
else return ()
|
|
|
|
|
|
|
|
encodePatchLocal :: UriMode u => Authority u -> PatchLocal -> Series
|
2020-08-13 19:26:20 +09:00
|
|
|
encodePatchLocal a (PatchLocal id_ context)
|
2020-07-14 18:56:13 +09:00
|
|
|
= "id" .= ObjURI a id_
|
|
|
|
<> "context" .= ObjURI a context
|
|
|
|
|
|
|
|
data Patch u = Patch
|
|
|
|
{ patchLocal :: Maybe (Authority u, PatchLocal)
|
|
|
|
, patchAttributedTo :: LocalURI
|
2020-07-15 22:00:58 +09:00
|
|
|
, patchPublished :: Maybe UTCTime
|
2020-08-15 06:16:33 +09:00
|
|
|
, patchType :: PatchMediaType
|
2020-05-25 18:40:48 +09:00
|
|
|
, patchContent :: Text
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub Patch where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
|
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Patch" :: Text)) $
|
|
|
|
fail "type isn't Patch"
|
|
|
|
|
2020-07-14 18:56:13 +09:00
|
|
|
ObjURI a attrib <- o .: "attributedTo"
|
2020-05-25 18:40:48 +09:00
|
|
|
|
|
|
|
fmap (a,) $
|
2020-07-14 18:56:13 +09:00
|
|
|
Patch
|
|
|
|
<$> parsePatchLocal o
|
|
|
|
<*> pure attrib
|
2020-07-15 22:00:58 +09:00
|
|
|
<*> o .:? "published"
|
2020-05-25 18:40:48 +09:00
|
|
|
<*> o .: "mediaType"
|
|
|
|
<*> o .: "content"
|
2020-07-14 17:50:57 +09:00
|
|
|
|
2020-07-14 18:56:13 +09:00
|
|
|
toSeries a (Patch local attrib published typ content)
|
|
|
|
= maybe mempty (uncurry encodePatchLocal) local
|
2020-07-14 17:50:57 +09:00
|
|
|
<> "type" .= ("Patch" :: Text)
|
2020-07-14 18:56:13 +09:00
|
|
|
<> "attributedTo" .= ObjURI a attrib
|
2020-07-15 22:00:58 +09:00
|
|
|
<> "published" .=? published
|
2020-07-14 17:50:57 +09:00
|
|
|
<> "mediaType" .= typ
|
|
|
|
<> "content" .= content
|
2020-05-25 18:40:48 +09:00
|
|
|
|
2020-08-13 19:26:20 +09:00
|
|
|
data BundleLocal = BundleLocal
|
|
|
|
{ bundleId :: LocalURI
|
|
|
|
, bundleContext :: LocalURI
|
|
|
|
, bundlePrevVersions :: [LocalURI]
|
|
|
|
, bundleCurrentVersion :: Maybe LocalURI
|
|
|
|
}
|
|
|
|
|
|
|
|
parseBundleLocal
|
|
|
|
:: UriMode u => Object -> Parser (Maybe (Authority u, BundleLocal))
|
|
|
|
parseBundleLocal o = do
|
|
|
|
mid <- o .:? "id"
|
|
|
|
case mid of
|
|
|
|
Nothing -> do
|
|
|
|
verifyNothing "context"
|
|
|
|
verifyNothing "previousVersions"
|
|
|
|
verifyNothing "currentVersion"
|
|
|
|
return Nothing
|
|
|
|
Just (ObjURI a id_) ->
|
|
|
|
fmap (Just . (a,)) $
|
|
|
|
BundleLocal
|
|
|
|
<$> pure id_
|
|
|
|
<*> withAuthorityO a (o .: "context")
|
|
|
|
<*> (traverse (withAuthorityO a . return) =<< o .:? "previousVersions" .!= [])
|
|
|
|
<*> withAuthorityMaybeO a (o .:? "currentVersion")
|
|
|
|
where
|
|
|
|
verifyNothing t =
|
|
|
|
if t `M.member` o
|
|
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
|
|
else return ()
|
|
|
|
|
|
|
|
encodeBundleLocal :: UriMode u => Authority u -> BundleLocal -> Series
|
|
|
|
encodeBundleLocal a (BundleLocal id_ context versions mcurrent)
|
|
|
|
= "id" .= ObjURI a id_
|
|
|
|
<> "context" .= ObjURI a context
|
|
|
|
<> "previousVersions" .= map (ObjURI a) versions
|
|
|
|
<> "currentVersion" .=? (ObjURI a <$> mcurrent)
|
|
|
|
|
|
|
|
data Bundle u
|
|
|
|
= BundleHosted (Maybe BundleLocal) (NonEmpty LocalURI)
|
|
|
|
| BundleOffer (Maybe (Authority u, BundleLocal)) (NonEmpty (Patch u))
|
|
|
|
|
|
|
|
instance ActivityPub Bundle where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
|
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("OrderedCollection" :: Text)) $
|
|
|
|
fail "type isn't OrderedCollection"
|
|
|
|
|
|
|
|
mlocal <- parseBundleLocal o
|
|
|
|
mtotal <- o .:? "totalItems"
|
|
|
|
|
|
|
|
items <- toEither <$> o .: "orderedItems" <|> o .: "items"
|
|
|
|
case items of
|
|
|
|
Left (ObjURI h lu :| us) -> do
|
|
|
|
for_ mlocal $ \ (h', _) ->
|
|
|
|
unless (h == h') $
|
|
|
|
fail "Patches in bundle not on the same host as bundle"
|
|
|
|
unless (all (== h) $ map objUriAuthority us) $
|
|
|
|
fail "Patches in bundle on different hosts"
|
|
|
|
for_ mtotal $ \ total ->
|
|
|
|
unless (length us + 1 == total) $
|
|
|
|
fail "Incorrect totalItems"
|
|
|
|
return (h, BundleHosted (snd <$> mlocal) $ lu :| map objUriLocal us)
|
|
|
|
Right (Doc h p :| ps) -> do
|
|
|
|
unless (all (== h) $ map docAuthority ps) $
|
|
|
|
fail "Patches in bundle have different authors"
|
|
|
|
for_ mtotal $ \ total ->
|
|
|
|
unless (length ps + 1 == total) $
|
|
|
|
fail "Incorrect totalItems"
|
|
|
|
return (h, BundleOffer mlocal $ p :| map docValue ps)
|
|
|
|
|
|
|
|
toSeries hBundle (BundleHosted mlocal lus)
|
|
|
|
= maybe mempty (encodeBundleLocal hBundle) mlocal
|
|
|
|
<> "type" .= ("OrderedCollection" :: Text)
|
|
|
|
<> "totalItems" .= length lus
|
|
|
|
<> "orderedItems" .= NE.map (ObjURI hBundle) lus
|
|
|
|
toSeries hAttrib (BundleOffer mlocal patches)
|
|
|
|
= maybe mempty (uncurry encodeBundleLocal) mlocal
|
|
|
|
<> "type" .= ("OrderedCollection" :: Text)
|
|
|
|
<> "totalItems" .= length patches
|
|
|
|
<> "orderedItems" .= NE.map (Doc hAttrib) patches
|
|
|
|
|
2019-06-06 19:25:16 +09:00
|
|
|
data TicketLocal = TicketLocal
|
|
|
|
{ ticketId :: LocalURI
|
|
|
|
, ticketReplies :: LocalURI
|
|
|
|
, ticketParticipants :: LocalURI
|
2020-05-24 18:17:49 +09:00
|
|
|
, ticketTeam :: Maybe LocalURI
|
2019-06-06 19:25:16 +09:00
|
|
|
, ticketEvents :: LocalURI
|
2019-07-12 00:53:55 +09:00
|
|
|
, ticketDeps :: LocalURI
|
|
|
|
, ticketReverseDeps :: LocalURI
|
2019-06-06 19:25:16 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseTicketLocal :: UriMode u => Object -> Parser (Maybe (Authority u, TicketLocal))
|
2019-06-06 19:25:16 +09:00
|
|
|
parseTicketLocal o = do
|
2019-07-23 22:59:48 +09:00
|
|
|
mid <- o .:? "id"
|
2019-06-06 19:25:16 +09:00
|
|
|
case mid of
|
|
|
|
Nothing -> do
|
|
|
|
verifyNothing "replies"
|
2019-06-12 09:11:24 +09:00
|
|
|
verifyNothing "participants"
|
2020-06-18 19:38:04 +09:00
|
|
|
verifyNothing "followers"
|
2019-06-12 09:11:24 +09:00
|
|
|
verifyNothing "team"
|
|
|
|
verifyNothing "history"
|
2019-07-12 00:53:55 +09:00
|
|
|
verifyNothing "dependencies"
|
|
|
|
verifyNothing "dependants"
|
2019-06-06 19:25:16 +09:00
|
|
|
return Nothing
|
2019-07-23 22:59:48 +09:00
|
|
|
Just (ObjURI a id_) ->
|
|
|
|
fmap (Just . (a,)) $
|
2019-06-06 19:25:16 +09:00
|
|
|
TicketLocal
|
|
|
|
<$> pure id_
|
2019-07-23 22:59:48 +09:00
|
|
|
<*> withAuthorityO a (o .: "replies")
|
2020-06-18 19:38:04 +09:00
|
|
|
<*> withAuthorityO a (o .: "participants" <|> o .: "followers")
|
2020-05-24 18:17:49 +09:00
|
|
|
<*> withAuthorityMaybeO a (o .:? "team")
|
2019-07-23 22:59:48 +09:00
|
|
|
<*> withAuthorityO a (o .: "history")
|
|
|
|
<*> withAuthorityO a (o .: "dependencies")
|
|
|
|
<*> withAuthorityO a (o .: "dependants")
|
2019-06-06 19:25:16 +09:00
|
|
|
where
|
|
|
|
verifyNothing t =
|
|
|
|
if t `M.member` o
|
|
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
|
|
else return ()
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeTicketLocal :: UriMode u => Authority u -> TicketLocal -> Series
|
2019-07-12 00:53:55 +09:00
|
|
|
encodeTicketLocal
|
2020-06-18 19:38:04 +09:00
|
|
|
a (TicketLocal id_ replies followers team events deps rdeps)
|
2019-07-23 22:59:48 +09:00
|
|
|
= "id" .= ObjURI a id_
|
|
|
|
<> "replies" .= ObjURI a replies
|
2020-06-18 19:38:04 +09:00
|
|
|
<> "followers" .= ObjURI a followers
|
2020-05-24 18:17:49 +09:00
|
|
|
<> "team" .=? (ObjURI a <$> team)
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "history" .= ObjURI a events
|
|
|
|
<> "dependencies" .= ObjURI a deps
|
|
|
|
<> "dependants" .= ObjURI a rdeps
|
|
|
|
|
2020-05-25 21:39:25 +09:00
|
|
|
data MergeRequest u = MergeRequest
|
2022-09-19 00:55:42 +09:00
|
|
|
{ mrOrigin :: Maybe (Either (ObjURI u) (Authority u, Branch u))
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
, mrTarget :: Either LocalURI (Branch u)
|
2022-09-19 00:55:42 +09:00
|
|
|
, mrBundle :: Maybe (Either (ObjURI u) (Authority u, Bundle u))
|
2020-05-25 21:39:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub MergeRequest where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
|
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Offer" :: Text)) $
|
|
|
|
fail "type isn't Offer"
|
2020-07-14 20:10:43 +09:00
|
|
|
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
target <- o .:+ "target"
|
|
|
|
let (a, target') =
|
|
|
|
case target of
|
|
|
|
Left (ObjURI h lu) -> (h, Left lu)
|
|
|
|
Right (Doc h branch) -> (h, Right branch)
|
2020-07-14 20:10:43 +09:00
|
|
|
|
|
|
|
fmap (a,) $
|
2020-05-25 21:39:25 +09:00
|
|
|
MergeRequest
|
2022-09-19 00:55:42 +09:00
|
|
|
<$> (fmap (second fromDoc) <$> o .:+? "origin")
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<*> pure target'
|
2022-09-19 00:55:42 +09:00
|
|
|
<*> (fmap (second fromDoc) <$> o .:+? "object")
|
2020-07-14 20:25:37 +09:00
|
|
|
where
|
|
|
|
fromDoc (Doc h v) = (h, v)
|
2020-05-25 21:39:25 +09:00
|
|
|
|
2020-08-13 19:26:20 +09:00
|
|
|
toSeries h (MergeRequest morigin target bundle)
|
2020-05-25 21:39:25 +09:00
|
|
|
= "type" .= ("Offer" :: Text)
|
2022-09-19 00:55:42 +09:00
|
|
|
<> "origin" .=+? fmap (second $ uncurry Doc) morigin
|
Switch to new actor layout
This is such a huge patch, it's probably impossible to tell what it does by
looking at the code. One thing is clear: It changes *everything* :P so here's
an overview:
- There are now 5 types of actors, each having its own top-level route
- So projects, repos, etc. are no longer "under" sharers
- Actor routes are now based on their KeyHashid, there are no "idents" anymore,
i.e. URLs look random and don't contain user or repo names
- No sharers anymore; people and groups are distinct entities not sharing a
common namespace or anything like that
- Project has been renamed to Deck and it simply means a ticket tracker; repos
are no longer "under" projects
- In addition to Person, Group, Repo and Deck, there's a new actor type Loom,
which is a patch tracker; i.e. Repo actors don't manage MRs anymore
- All C2S and S2S is temporarily disabled, because huge changes to the whole
code are required and I'll do them gradually in the next patches
- Since form-based actions are implemented using C2S, they're disabled as well,
so Vervis is now essentially read-only
- Some views have been temporarily removed, e.g. repo history and commit view
- A huge set of DB migrations has been added to adapt the DB to these changes;
I haven't tested them yet on a read DB so there may be errors there; I'll fix
them in the next patches if I find any (probably going to test on the main
instance where Vervis itself is hosted...)
- Some modules got tech upgrades, e.g. LocalActor became a higher-kinded type
and a similar pattern is probably relevant for several other types
- There's an 'Actor' entity in the DB schema now, and all 5 actor types use it
for common things like inbox and outbox
- Although inbox and outbox are used only by Actor, so essentially could be
removed, I haven't removed them; that's because I wonder if at some point
users can have a tree of inboxes much like in email; I don't have an excuse
for Outbox, but anyway, leaving them as is for now
- Workflows, roles and collaborators are partially removed/unused until I
figure out a sane federated way to provide these features
- Since repo routes don't contain a "sharer" anymore, SSH URIs are now simpler,
they already look like user@host/repo regardless of who "controls" that repo
2022-08-15 22:57:42 +09:00
|
|
|
<> "target" .=+ bimap (ObjURI h) (Doc h) target
|
2022-09-19 00:55:42 +09:00
|
|
|
<> "object" .=+? fmap (second $ uncurry Doc) bundle
|
2020-05-25 21:39:25 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Ticket u = Ticket
|
|
|
|
{ ticketLocal :: Maybe (Authority u, TicketLocal)
|
2019-06-06 19:25:16 +09:00
|
|
|
, ticketAttributedTo :: LocalURI
|
2019-06-04 06:52:34 +09:00
|
|
|
, ticketPublished :: Maybe UTCTime
|
|
|
|
, ticketUpdated :: Maybe UTCTime
|
2020-02-11 23:14:52 +09:00
|
|
|
, ticketContext :: Maybe (ObjURI u)
|
2020-02-03 23:53:12 +09:00
|
|
|
-- , ticketName :: Maybe Text
|
2022-09-21 21:50:26 +09:00
|
|
|
, ticketSummary :: Escaped
|
|
|
|
, ticketContent :: HTML
|
|
|
|
, ticketSource :: PandocMarkdown
|
2019-07-23 22:59:48 +09:00
|
|
|
, ticketAssignedTo :: Maybe (ObjURI u)
|
2020-08-05 19:11:16 +09:00
|
|
|
, ticketResolved :: Maybe (Maybe (ObjURI u), Maybe UTCTime)
|
2020-05-25 21:39:25 +09:00
|
|
|
, ticketAttachment :: Maybe (Authority u, MergeRequest u)
|
2019-06-04 06:52:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub Ticket where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
2019-06-04 06:52:34 +09:00
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Ticket" :: Text)) $
|
|
|
|
fail "type isn't Ticket"
|
|
|
|
|
|
|
|
mediaType <- o .: "mediaType"
|
|
|
|
unless (mediaType == ("text/html" :: Text)) $
|
|
|
|
fail "mediaType isn't HTML"
|
|
|
|
|
|
|
|
source <- o .: "source"
|
|
|
|
sourceType <- source .: "mediaType"
|
|
|
|
unless (sourceType == ("text/markdown; variant=Pandoc" :: Text)) $
|
|
|
|
fail "source mediaType isn't Pandoc Markdown"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
ObjURI a attributedTo <- o .: "attributedTo"
|
2019-06-04 06:52:34 +09:00
|
|
|
|
2020-08-05 19:11:16 +09:00
|
|
|
mresolved <- do
|
|
|
|
is <- o .:? "isResolved" .!= False
|
|
|
|
if is
|
|
|
|
then do
|
|
|
|
at <- o .:? "resolved"
|
|
|
|
by <- o .:? "resolvedBy"
|
|
|
|
return $ Just (by, at)
|
|
|
|
else do
|
|
|
|
verifyNothing "resolved"
|
|
|
|
verifyNothing "resolvedBy"
|
|
|
|
return Nothing
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fmap (a,) $
|
2019-06-04 06:52:34 +09:00
|
|
|
Ticket
|
2019-06-06 19:25:16 +09:00
|
|
|
<$> parseTicketLocal o
|
|
|
|
<*> pure attributedTo
|
2019-06-04 06:52:34 +09:00
|
|
|
<*> o .:? "published"
|
|
|
|
<*> o .:? "updated"
|
2020-02-11 23:14:52 +09:00
|
|
|
<*> o .:? "context"
|
2020-02-03 23:53:12 +09:00
|
|
|
-- <*> o .:? "name"
|
2022-09-21 21:50:26 +09:00
|
|
|
<*> o .: "summary"
|
|
|
|
<*> o .: "content"
|
2019-06-04 06:52:34 +09:00
|
|
|
<*> source .: "content"
|
2019-06-12 09:11:24 +09:00
|
|
|
<*> o .:? "assignedTo"
|
2020-08-05 19:11:16 +09:00
|
|
|
<*> pure mresolved
|
2020-05-25 21:39:25 +09:00
|
|
|
<*> (traverse parseObject =<< o .:? "attachment")
|
2020-08-05 19:11:16 +09:00
|
|
|
where
|
|
|
|
verifyNothing t =
|
|
|
|
if t `M.member` o
|
|
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
|
|
else return ()
|
2019-06-04 06:52:34 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
toSeries authority
|
2020-02-11 23:14:52 +09:00
|
|
|
(Ticket local attributedTo published updated context {-name-}
|
2020-08-05 19:11:16 +09:00
|
|
|
summary content source assignedTo mresolved mmr)
|
2019-06-06 19:25:16 +09:00
|
|
|
|
|
|
|
= maybe mempty (uncurry encodeTicketLocal) local
|
|
|
|
<> "type" .= ("Ticket" :: Text)
|
2019-07-23 22:59:48 +09:00
|
|
|
<> "attributedTo" .= ObjURI authority attributedTo
|
2019-06-04 06:52:34 +09:00
|
|
|
<> "published" .=? published
|
|
|
|
<> "updated" .=? updated
|
2020-02-11 23:14:52 +09:00
|
|
|
<> "context" .=? context
|
2020-02-03 23:53:12 +09:00
|
|
|
-- <> "name" .=? name
|
2019-06-04 06:52:34 +09:00
|
|
|
<> "summary" .= summary
|
|
|
|
<> "content" .= content
|
|
|
|
<> "mediaType" .= ("text/html" :: Text)
|
|
|
|
<> "source" .= object
|
|
|
|
[ "content" .= source
|
|
|
|
, "mediaType" .= ("text/markdown; variant=Pandoc" :: Text)
|
|
|
|
]
|
2019-06-12 09:11:24 +09:00
|
|
|
<> "assignedTo" .=? assignedTo
|
2020-08-05 19:11:16 +09:00
|
|
|
<> maybe
|
|
|
|
("isResolved" .= False)
|
|
|
|
(\ (mby, mat)
|
|
|
|
-> "isResolved" .= True
|
|
|
|
<> "resolvedBy" .=? mby
|
|
|
|
<> "resolved" .=? mat
|
|
|
|
)
|
|
|
|
mresolved
|
2020-05-25 21:39:25 +09:00
|
|
|
<> maybe
|
|
|
|
mempty
|
|
|
|
(\ (h, mr) -> "attachment" `pair` pairs (toSeries h mr))
|
|
|
|
mmr
|
2019-06-04 06:52:34 +09:00
|
|
|
|
2019-08-06 22:23:11 +09:00
|
|
|
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
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
2019-08-06 22:23:11 +09:00
|
|
|
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_
|
2019-08-29 01:20:19 +09:00
|
|
|
<$> withAuthorityO a (o .: "context")
|
2019-08-06 22:23:11 +09:00
|
|
|
<*> 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)
|
2019-10-11 01:48:57 +09:00
|
|
|
<> "context" .= ObjURI authority repo
|
2019-08-06 22:23:11 +09:00
|
|
|
<> "attributedTo" .=+ author
|
|
|
|
<> "committedBy" .=+? committer
|
|
|
|
<> "name" .= title
|
|
|
|
<> "hash" .= hash
|
|
|
|
<> maybe
|
|
|
|
mempty
|
|
|
|
(\ desc -> "description" .= object
|
|
|
|
[ "content" .= desc
|
|
|
|
, "mediaType" .= ("text/plain" :: Text)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
mdesc
|
|
|
|
<> "created" .= written
|
|
|
|
<> "committed" .=? mcommitted
|
|
|
|
|
2019-08-29 00:31:40 +09:00
|
|
|
data Branch u = Branch
|
|
|
|
{ branchName :: Text
|
|
|
|
, branchRef :: Text
|
|
|
|
, branchRepo :: LocalURI
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub Branch where
|
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Branch" :: Text)) $
|
|
|
|
fail "type isn't Branch"
|
|
|
|
|
|
|
|
ObjURI a repo <- o .: "context"
|
|
|
|
fmap (a,) $
|
|
|
|
Branch
|
|
|
|
<$> o .: "name"
|
|
|
|
<*> o .: "ref"
|
|
|
|
<*> pure repo
|
|
|
|
|
|
|
|
toSeries authority (Branch name ref repo)
|
|
|
|
= "type" .= ("Branch" :: Text)
|
|
|
|
<> "name" .= name
|
|
|
|
<> "ref" .= ref
|
|
|
|
<> "context" .= ObjURI authority repo
|
|
|
|
|
2022-07-26 02:15:22 +09:00
|
|
|
data Role = RoleAdmin deriving Eq
|
|
|
|
|
|
|
|
instance FromJSON Role where
|
|
|
|
parseJSON = withText "Role" parse
|
|
|
|
where
|
|
|
|
parse "https://forgefed.org/ns#admin" = pure RoleAdmin
|
|
|
|
parse t = fail $ "Unknown role: " ++ T.unpack t
|
|
|
|
|
|
|
|
instance ToJSON Role where
|
|
|
|
toJSON = error "toJSON Role"
|
|
|
|
toEncoding r =
|
|
|
|
toEncoding $ case r of
|
|
|
|
RoleAdmin -> "https://forgefed.org/ns#admin" :: Text
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Accept u = Accept
|
|
|
|
{ acceptObject :: ObjURI u
|
2019-09-11 17:12:20 +09:00
|
|
|
, acceptResult :: Maybe LocalURI
|
2019-03-14 08:37:58 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseAccept :: UriMode u => Authority u -> Object -> Parser (Accept u)
|
|
|
|
parseAccept a o =
|
2019-06-26 10:12:11 +09:00
|
|
|
Accept
|
|
|
|
<$> o .: "object"
|
2019-09-11 17:12:20 +09:00
|
|
|
<*> withAuthorityMaybeO a (o .:? "result")
|
2019-03-14 08:37:58 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeAccept :: UriMode u => Authority u -> Accept u -> Series
|
2019-09-11 17:12:20 +09:00
|
|
|
encodeAccept authority (Accept obj mresult)
|
|
|
|
= "object" .= obj
|
|
|
|
<> "result" .=? (ObjURI authority <$> mresult)
|
2019-03-14 08:37:58 +09:00
|
|
|
|
2020-09-10 19:57:02 +09:00
|
|
|
data AddObject u = AddBundle (NonEmpty (Patch u))
|
|
|
|
|
|
|
|
instance ActivityPub AddObject where
|
|
|
|
jsonldContext = error "jsonldContext AddObject"
|
|
|
|
parseObject o = do
|
|
|
|
(h, b) <- parseObject o
|
|
|
|
patches <-
|
|
|
|
case b of
|
|
|
|
BundleHosted _ _ -> fail "Patches specified as URIs"
|
|
|
|
BundleOffer mlocal pts -> do
|
|
|
|
for_ mlocal $ \ _ -> fail "Bundle 'id' specified"
|
|
|
|
return pts
|
|
|
|
return (h, AddBundle patches)
|
|
|
|
toSeries h (AddBundle ps) = toSeries h $ BundleOffer Nothing ps
|
|
|
|
|
|
|
|
data Add u = Add
|
|
|
|
{ addObject :: Either (ObjURI u) (AddObject u)
|
|
|
|
, addTarget :: ObjURI u
|
|
|
|
}
|
|
|
|
|
|
|
|
parseAdd :: UriMode u => Object -> Authority u -> Parser (Add u)
|
|
|
|
parseAdd o h = Add
|
|
|
|
<$> (bitraverse pure (withAuthorityD h . pure) =<<
|
|
|
|
toEither <$> o .: "object"
|
|
|
|
)
|
|
|
|
<*> o .: "target"
|
|
|
|
|
|
|
|
encodeAdd :: UriMode u => Authority u -> Add u -> Series
|
|
|
|
encodeAdd h (Add obj target)
|
|
|
|
= case obj of
|
|
|
|
Left u -> "object" .= u
|
|
|
|
Right o -> "object" `pair` pairs (toSeries h o)
|
|
|
|
<> "target" .= target
|
|
|
|
|
2022-06-23 18:09:02 +09:00
|
|
|
data Apply u = Apply
|
|
|
|
{ applyObject :: ObjURI u
|
|
|
|
, applyTarget :: ObjURI u
|
|
|
|
}
|
|
|
|
|
|
|
|
parseApply :: UriMode u => Object -> Parser (Apply u)
|
|
|
|
parseApply o =
|
|
|
|
Apply
|
|
|
|
<$> o .: "object"
|
|
|
|
<*> o .: "target"
|
|
|
|
|
|
|
|
encodeApply :: UriMode u => Apply u -> Series
|
|
|
|
encodeApply (Apply obj target)
|
|
|
|
= "object" .= obj
|
|
|
|
<> "target" .= target
|
|
|
|
|
2022-07-25 01:52:28 +09:00
|
|
|
data CreateObject u
|
|
|
|
= CreateNote (Authority u) (Note u)
|
|
|
|
| CreateTicket (Authority u) (Ticket u)
|
|
|
|
| CreateTicketTracker ActorDetail (Maybe (Authority u, ActorLocal u))
|
2022-09-16 19:34:44 +09:00
|
|
|
| CreateRepository ActorDetail VersionControlSystem (Maybe (Authority u, ActorLocal u))
|
2022-09-17 17:31:22 +09:00
|
|
|
| CreatePatchTracker ActorDetail (NonEmpty (ObjURI u)) (Maybe (Authority u, ActorLocal u))
|
2022-07-25 01:52:28 +09:00
|
|
|
|
|
|
|
parseCreateObject :: UriMode u => Object -> Parser (CreateObject u)
|
|
|
|
parseCreateObject o
|
|
|
|
= uncurry CreateNote <$> parseObject o
|
|
|
|
<|> uncurry CreateTicket <$> parseObject o
|
|
|
|
<|> do d <- parseActorDetail o
|
|
|
|
unless (actorType d == ActorTypeTicketTracker) $
|
|
|
|
fail "type isn't TicketTracker"
|
|
|
|
ml <- parseActorLocal o
|
|
|
|
return $ CreateTicketTracker d ml
|
2022-09-16 19:34:44 +09:00
|
|
|
<|> do d <- parseActorDetail o
|
|
|
|
unless (actorType d == ActorTypeRepo) $
|
|
|
|
fail "type isn't Repository"
|
|
|
|
vcs <- o .: "versionControlSystem"
|
|
|
|
ml <- parseActorLocal o
|
|
|
|
return $ CreateRepository d vcs ml
|
2022-09-17 17:31:22 +09:00
|
|
|
<|> do d <- parseActorDetail o
|
|
|
|
unless (actorType d == ActorTypePatchTracker) $
|
|
|
|
fail "type isn't PatchTracker"
|
|
|
|
repos <- o .:*+ "tracksPatchesFor"
|
|
|
|
ml <- parseActorLocal o
|
|
|
|
return $ CreatePatchTracker d repos ml
|
2022-07-25 01:52:28 +09:00
|
|
|
|
|
|
|
encodeCreateObject :: UriMode u => CreateObject u -> Series
|
|
|
|
encodeCreateObject (CreateNote h note) = toSeries h note
|
|
|
|
encodeCreateObject (CreateTicket h ticket) = toSeries h ticket
|
|
|
|
encodeCreateObject (CreateTicketTracker d ml) =
|
|
|
|
encodeActorDetail d <> maybe mempty (uncurry encodeActorLocal) ml
|
2022-09-16 19:34:44 +09:00
|
|
|
encodeCreateObject (CreateRepository d vcs ml)
|
|
|
|
= encodeActorDetail d
|
|
|
|
<> "versionControlSystem" .= vcs
|
|
|
|
<> maybe mempty (uncurry encodeActorLocal) ml
|
2022-09-17 17:31:22 +09:00
|
|
|
encodeCreateObject (CreatePatchTracker d repos ml)
|
|
|
|
= encodeActorDetail d
|
|
|
|
<> "tracksPatchesFor" .=*+ repos
|
|
|
|
<> maybe mempty (uncurry encodeActorLocal) ml
|
2020-02-10 23:51:32 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Create u = Create
|
2020-02-10 23:51:32 +09:00
|
|
|
{ createObject :: CreateObject u
|
2020-02-18 22:26:28 +09:00
|
|
|
, createTarget :: Maybe (ObjURI u)
|
2019-02-12 20:53:24 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseCreate :: UriMode u => Object -> Authority u -> LocalURI -> Parser (Create u)
|
|
|
|
parseCreate o a luActor = do
|
2022-07-25 01:52:28 +09:00
|
|
|
obj <- parseCreateObject =<< o .: "object"
|
|
|
|
case obj of
|
|
|
|
CreateNote h note ->
|
|
|
|
unless (a == h && luActor == noteAttrib note) $
|
|
|
|
fail "Create actor != note attrib"
|
|
|
|
CreateTicket h ticket ->
|
|
|
|
unless (a == h && luActor == ticketAttributedTo ticket) $
|
|
|
|
fail "Create actor != note attrib"
|
|
|
|
CreateTicketTracker _ _ -> return ()
|
2022-09-16 19:34:44 +09:00
|
|
|
CreateRepository _ _ _ -> return ()
|
2022-09-17 17:31:22 +09:00
|
|
|
CreatePatchTracker _ _ _ -> return ()
|
2020-02-18 22:26:28 +09:00
|
|
|
Create obj <$> o .:? "target"
|
2019-03-10 15:42:03 +09:00
|
|
|
|
2022-07-25 01:52:28 +09:00
|
|
|
encodeCreate :: UriMode u => Create u -> Series
|
|
|
|
encodeCreate (Create obj target)
|
|
|
|
= "object" `pair` pairs (encodeCreateObject obj)
|
2020-02-18 22:26:28 +09:00
|
|
|
<> "target" .=? target
|
2019-02-12 20:53:24 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Follow u = Follow
|
2019-09-25 19:43:05 +09:00
|
|
|
{ followObject :: ObjURI u
|
|
|
|
, followContext :: Maybe (ObjURI u)
|
|
|
|
, followHide :: Bool
|
2019-03-14 08:37:58 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseFollow :: UriMode u => Object -> Parser (Follow u)
|
2019-03-14 08:37:58 +09:00
|
|
|
parseFollow o =
|
|
|
|
Follow
|
2019-09-25 19:43:05 +09:00
|
|
|
<$> o .: "object"
|
|
|
|
<*> o .:? "context"
|
2019-11-03 22:43:59 +09:00
|
|
|
<*> o .:? "hide" .!= False
|
2019-03-14 08:37:58 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeFollow :: UriMode u => Follow u -> Series
|
2019-09-25 19:43:05 +09:00
|
|
|
encodeFollow (Follow obj mcontext hide)
|
|
|
|
= "object" .= obj
|
|
|
|
<> "context" .=? mcontext
|
|
|
|
<> "hide" .= hide
|
2019-03-14 08:37:58 +09:00
|
|
|
|
2022-07-26 02:15:22 +09:00
|
|
|
data Grant u = Grant
|
|
|
|
{ grantObject :: Either Role (ObjURI u)
|
|
|
|
, grantContext :: ObjURI u
|
|
|
|
, grantTarget :: ObjURI u
|
|
|
|
}
|
|
|
|
|
|
|
|
parseGrant :: UriMode u => Object -> Parser (Grant u)
|
|
|
|
parseGrant o =
|
|
|
|
Grant
|
2022-09-06 01:19:52 +09:00
|
|
|
<$> o .:+ "object"
|
2022-07-26 02:15:22 +09:00
|
|
|
<*> o .: "context"
|
|
|
|
<*> o .: "target"
|
|
|
|
|
|
|
|
encodeGrant :: UriMode u => Grant u -> Series
|
2022-08-16 23:55:21 +09:00
|
|
|
encodeGrant (Grant obj context target)
|
2022-09-06 01:19:52 +09:00
|
|
|
= "object" .=+ obj
|
|
|
|
<> "context" .= context
|
|
|
|
<> "target" .= target
|
|
|
|
|
|
|
|
data Invite u = Invite
|
|
|
|
{ inviteInstrument :: Either Role (ObjURI u)
|
|
|
|
, inviteObject :: ObjURI u
|
|
|
|
, inviteTarget :: ObjURI u
|
|
|
|
}
|
|
|
|
|
|
|
|
parseInvite :: UriMode u => Object -> Parser (Invite u)
|
|
|
|
parseInvite o =
|
|
|
|
Invite
|
|
|
|
<$> o .:+ "instrument"
|
|
|
|
<*> o .: "object"
|
|
|
|
<*> o .: "target"
|
|
|
|
|
|
|
|
encodeInvite :: UriMode u => Invite u -> Series
|
|
|
|
encodeInvite (Invite obj context target)
|
|
|
|
= "object" .=+ obj
|
2022-07-26 02:15:22 +09:00
|
|
|
<> "context" .= context
|
|
|
|
<> "target" .= target
|
|
|
|
|
2020-06-18 19:38:04 +09:00
|
|
|
data OfferObject u = OfferTicket (Ticket u) | OfferDep (TicketDependency u)
|
|
|
|
|
|
|
|
instance ActivityPub OfferObject where
|
|
|
|
jsonldContext = error "jsonldContext OfferObject"
|
|
|
|
parseObject o
|
|
|
|
= second OfferTicket <$> parseObject o
|
|
|
|
<|> second OfferDep <$> parseObject o
|
|
|
|
toSeries h (OfferTicket t) = toSeries h t
|
|
|
|
toSeries h (OfferDep d) = toSeries h d
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Offer u = Offer
|
2020-06-18 19:38:04 +09:00
|
|
|
{ offerObject :: OfferObject u
|
2019-07-23 22:59:48 +09:00
|
|
|
, offerTarget :: ObjURI u
|
2019-06-06 23:16:48 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseOffer :: UriMode u => Object -> Authority u -> LocalURI -> Parser (Offer u)
|
|
|
|
parseOffer o a luActor = do
|
2020-06-18 19:38:04 +09:00
|
|
|
obj <- withAuthorityT a $ parseObject =<< o .: "object"
|
2019-07-23 22:59:48 +09:00
|
|
|
target@(ObjURI hTarget luTarget) <- o .: "target"
|
2020-06-18 19:38:04 +09:00
|
|
|
case obj of
|
|
|
|
OfferTicket ticket -> do
|
|
|
|
unless (luActor == ticketAttributedTo ticket) $
|
|
|
|
fail "Offer actor != Ticket attrib"
|
|
|
|
for_ (ticketContext ticket) $ \ (ObjURI hContext luContext) -> do
|
|
|
|
unless (hTarget == hContext) $
|
|
|
|
fail "Offer target host != Ticket context host"
|
|
|
|
unless (luTarget == luContext) $
|
|
|
|
fail "Offer target != Ticket context"
|
|
|
|
OfferDep dep -> do
|
|
|
|
unless (luActor == ticketDepAttributedTo dep) $
|
|
|
|
fail "Offer actor != TicketDependency attrib"
|
|
|
|
return $ Offer obj target
|
2019-06-06 23:16:48 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeOffer :: UriMode u => Authority u -> LocalURI -> Offer u -> Series
|
|
|
|
encodeOffer authority actor (Offer obj target)
|
|
|
|
= "object" `pair` pairs (toSeries authority obj)
|
2019-06-06 23:16:48 +09:00
|
|
|
<> "target" .= target
|
|
|
|
|
2019-08-29 00:31:40 +09:00
|
|
|
data Push u = Push
|
2019-09-09 09:27:45 +09:00
|
|
|
{ pushCommitsLast :: NonEmpty (Commit u)
|
|
|
|
, pushCommitsFirst :: Maybe (NonEmpty (Commit u))
|
2019-08-29 00:31:40 +09:00
|
|
|
, pushCommitsTotal :: Int
|
|
|
|
, pushTarget :: LocalURI
|
2019-10-22 19:28:35 +09:00
|
|
|
, pushContext :: LocalURI
|
2019-09-09 09:27:45 +09:00
|
|
|
, pushHashBefore :: Maybe Text
|
2019-10-11 01:41:34 +09:00
|
|
|
, pushHashAfter :: Maybe Text
|
2019-08-29 00:31:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
parsePush :: UriMode u => Authority u -> Object -> Parser (Push u)
|
|
|
|
parsePush a o = do
|
|
|
|
c <- o .: "object"
|
|
|
|
Push
|
2019-11-07 05:46:21 +09:00
|
|
|
<$> (traverse (withAuthorityT a . parseObject) =<< c .: "items" <|> c .: "orderedItems")
|
2019-09-09 09:27:45 +09:00
|
|
|
<*> (traverse (traverse $ withAuthorityT a . parseObject) =<< c .:? "earlyItems")
|
2019-08-29 00:31:40 +09:00
|
|
|
<*> c .: "totalItems"
|
|
|
|
<*> withAuthorityO a (o .: "target")
|
2019-10-22 19:28:35 +09:00
|
|
|
<*> withAuthorityO a (o .: "context")
|
2019-09-09 09:27:45 +09:00
|
|
|
<*> o .:? "hashBefore"
|
2019-10-11 01:41:34 +09:00
|
|
|
<*> o .:? "hashAfter"
|
2019-08-29 00:31:40 +09:00
|
|
|
|
|
|
|
encodePush :: UriMode u => Authority u -> Push u -> Series
|
2019-10-22 19:28:35 +09:00
|
|
|
encodePush a (Push lateCommits earlyCommits total target context before after)
|
2019-08-29 00:31:40 +09:00
|
|
|
= "object" `pair` pairs
|
|
|
|
( "type" .= ("OrderedCollection" :: Text)
|
2019-11-07 05:46:21 +09:00
|
|
|
<> pair "orderedItems" (objectList lateCommits)
|
2019-09-09 09:27:45 +09:00
|
|
|
<> maybe mempty (pair "earlyItems" . objectList) earlyCommits
|
2019-08-29 00:31:40 +09:00
|
|
|
<> "totalItems" .= total
|
|
|
|
)
|
|
|
|
<> "target" .= ObjURI a target
|
2019-10-22 19:28:35 +09:00
|
|
|
<> "context" .= ObjURI a context
|
2019-09-09 09:27:45 +09:00
|
|
|
<> "hashBefore" .=? before
|
2019-10-11 01:41:34 +09:00
|
|
|
<> "hashAfter" .=? after
|
2019-09-09 09:27:45 +09:00
|
|
|
where
|
|
|
|
objectList items = listEncoding (pairs . toSeries a) (NE.toList items)
|
2019-08-29 00:31:40 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Reject u = Reject
|
|
|
|
{ rejectObject :: ObjURI u
|
2019-03-14 08:37:58 +09:00
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
parseReject :: UriMode u => Object -> Parser (Reject u)
|
2019-03-14 08:37:58 +09:00
|
|
|
parseReject o = Reject <$> o .: "object"
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
encodeReject :: UriMode u => Reject u -> Series
|
2019-03-14 08:37:58 +09:00
|
|
|
encodeReject (Reject obj) = "object" .= obj
|
|
|
|
|
2020-07-23 23:27:11 +09:00
|
|
|
data Resolve u = Resolve
|
|
|
|
{ resolveObject :: ObjURI u
|
|
|
|
}
|
|
|
|
|
|
|
|
parseResolve :: UriMode u => Object -> Parser (Resolve u)
|
|
|
|
parseResolve o = Resolve <$> o .: "object"
|
|
|
|
|
|
|
|
encodeResolve :: UriMode u => Resolve u -> Series
|
|
|
|
encodeResolve (Resolve obj) = "object" .= obj
|
|
|
|
|
2019-09-25 19:43:05 +09:00
|
|
|
data Undo u = Undo
|
2020-08-05 17:28:58 +09:00
|
|
|
{ undoObject :: ObjURI u
|
2019-09-25 19:43:05 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
parseUndo :: UriMode u => Authority u -> Object -> Parser (Undo u)
|
2020-08-05 17:28:58 +09:00
|
|
|
parseUndo a o = Undo <$> o .: "object"
|
2019-09-25 19:43:05 +09:00
|
|
|
|
|
|
|
encodeUndo :: UriMode u => Authority u -> Undo u -> Series
|
2020-08-05 17:28:58 +09:00
|
|
|
encodeUndo a (Undo obj) = "object" .= obj
|
2019-09-25 19:43:05 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data SpecificActivity u
|
2022-06-23 18:09:02 +09:00
|
|
|
= AcceptActivity (Accept u)
|
2020-09-10 19:57:02 +09:00
|
|
|
| AddActivity (Add u)
|
2022-06-23 18:09:02 +09:00
|
|
|
| ApplyActivity (Apply u)
|
|
|
|
| CreateActivity (Create u)
|
|
|
|
| FollowActivity (Follow u)
|
2022-07-26 02:15:22 +09:00
|
|
|
| GrantActivity (Grant u)
|
2022-09-06 01:19:52 +09:00
|
|
|
| InviteActivity (Invite u)
|
2022-06-23 18:09:02 +09:00
|
|
|
| OfferActivity (Offer u)
|
|
|
|
| PushActivity (Push u)
|
|
|
|
| RejectActivity (Reject u)
|
2020-07-23 23:27:11 +09:00
|
|
|
| ResolveActivity (Resolve u)
|
2022-06-23 18:09:02 +09:00
|
|
|
| UndoActivity (Undo u)
|
2019-03-14 08:37:58 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
data Activity u = Activity
|
2022-06-22 16:39:38 +09:00
|
|
|
{ activityId :: Maybe LocalURI
|
|
|
|
, activityActor :: LocalURI
|
|
|
|
, activityCapability :: Maybe (ObjURI u)
|
2022-09-21 21:50:26 +09:00
|
|
|
, activitySummary :: Maybe HTML
|
2022-06-22 16:39:38 +09:00
|
|
|
, activityAudience :: Audience u
|
2022-08-16 23:55:21 +09:00
|
|
|
, activityFulfills :: [ObjURI u]
|
2022-06-22 16:39:38 +09:00
|
|
|
, activitySpecific :: SpecificActivity u
|
2019-03-14 08:37:58 +09:00
|
|
|
}
|
2019-02-12 20:53:24 +09:00
|
|
|
|
2019-03-10 15:42:03 +09:00
|
|
|
instance ActivityPub Activity where
|
2022-07-25 18:10:24 +09:00
|
|
|
jsonldContext _ = [as2Context, forgeContext]
|
2019-03-10 15:42:03 +09:00
|
|
|
parseObject o = do
|
2019-07-23 22:59:48 +09:00
|
|
|
ObjURI a actor <- o .: "actor"
|
|
|
|
fmap (a,) $
|
2019-06-19 17:53:31 +09:00
|
|
|
Activity
|
2019-07-23 22:59:48 +09:00
|
|
|
<$> withAuthorityMaybeO a (o .:? "id")
|
2019-06-19 17:53:31 +09:00
|
|
|
<*> pure actor
|
2022-06-22 16:39:38 +09:00
|
|
|
<*> o .:? "capability"
|
2022-09-21 21:50:26 +09:00
|
|
|
<*> o .:? "summary"
|
2019-06-16 01:24:34 +09:00
|
|
|
<*> parseAudience o
|
2022-08-16 23:55:21 +09:00
|
|
|
<*> o .:? "fulfills" .!= []
|
2019-03-14 11:30:36 +09:00
|
|
|
<*> do
|
|
|
|
typ <- o .: "type"
|
|
|
|
case typ of
|
2020-07-23 23:27:11 +09:00
|
|
|
"Accept" -> AcceptActivity <$> parseAccept a o
|
2020-09-10 19:57:02 +09:00
|
|
|
"Add" -> AddActivity <$> parseAdd o a
|
2022-06-23 18:09:02 +09:00
|
|
|
"Apply" -> ApplyActivity <$> parseApply o
|
2020-07-23 23:27:11 +09:00
|
|
|
"Create" -> CreateActivity <$> parseCreate o a actor
|
|
|
|
"Follow" -> FollowActivity <$> parseFollow o
|
2022-07-26 02:15:22 +09:00
|
|
|
"Grant" -> GrantActivity <$> parseGrant o
|
2022-09-06 01:19:52 +09:00
|
|
|
"Invite" -> InviteActivity <$> parseInvite o
|
2020-07-23 23:27:11 +09:00
|
|
|
"Offer" -> OfferActivity <$> parseOffer o a actor
|
|
|
|
"Push" -> PushActivity <$> parsePush a o
|
|
|
|
"Reject" -> RejectActivity <$> parseReject o
|
|
|
|
"Resolve" -> ResolveActivity <$> parseResolve o
|
|
|
|
"Undo" -> UndoActivity <$> parseUndo a o
|
2019-03-14 11:30:36 +09:00
|
|
|
_ ->
|
|
|
|
fail $
|
|
|
|
"Unrecognized activity type: " ++ T.unpack typ
|
2022-08-16 23:55:21 +09:00
|
|
|
toSeries authority (Activity id_ actor mcap summary audience fulfills specific)
|
2022-06-22 16:39:38 +09:00
|
|
|
= "type" .= activityType specific
|
|
|
|
<> "id" .=? (ObjURI authority <$> id_)
|
|
|
|
<> "actor" .= ObjURI authority actor
|
|
|
|
<> "capability" .=? mcap
|
|
|
|
<> "summary" .=? summary
|
2019-03-14 11:30:36 +09:00
|
|
|
<> encodeAudience audience
|
2022-08-16 23:55:21 +09:00
|
|
|
<> "fulfills" .=% fulfills
|
2019-07-23 22:59:48 +09:00
|
|
|
<> encodeSpecific authority actor specific
|
2019-03-14 08:37:58 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
activityType :: SpecificActivity u -> Text
|
2020-07-23 23:27:11 +09:00
|
|
|
activityType (AcceptActivity _) = "Accept"
|
2020-09-10 19:57:02 +09:00
|
|
|
activityType (AddActivity _) = "Add"
|
2022-06-23 18:09:02 +09:00
|
|
|
activityType (ApplyActivity _) = "Apply"
|
2020-07-23 23:27:11 +09:00
|
|
|
activityType (CreateActivity _) = "Create"
|
|
|
|
activityType (FollowActivity _) = "Follow"
|
2022-07-26 02:15:22 +09:00
|
|
|
activityType (GrantActivity _) = "Grant"
|
2022-09-06 01:19:52 +09:00
|
|
|
activityType (InviteActivity _) = "Invite"
|
2020-07-23 23:27:11 +09:00
|
|
|
activityType (OfferActivity _) = "Offer"
|
|
|
|
activityType (PushActivity _) = "Push"
|
|
|
|
activityType (RejectActivity _) = "Reject"
|
|
|
|
activityType (ResolveActivity _) = "Resolve"
|
|
|
|
activityType (UndoActivity _) = "Undo"
|
|
|
|
encodeSpecific h _ (AcceptActivity a) = encodeAccept h a
|
2020-09-10 19:57:02 +09:00
|
|
|
encodeSpecific h _ (AddActivity a) = encodeAdd h a
|
2022-06-23 18:09:02 +09:00
|
|
|
encodeSpecific _ _ (ApplyActivity a) = encodeApply a
|
2022-07-25 01:52:28 +09:00
|
|
|
encodeSpecific _ _ (CreateActivity a) = encodeCreate a
|
2020-07-23 23:27:11 +09:00
|
|
|
encodeSpecific _ _ (FollowActivity a) = encodeFollow a
|
2022-07-26 02:15:22 +09:00
|
|
|
encodeSpecific _ _ (GrantActivity a) = encodeGrant a
|
2022-09-06 01:19:52 +09:00
|
|
|
encodeSpecific _ _ (InviteActivity a) = encodeInvite a
|
2020-07-23 23:27:11 +09:00
|
|
|
encodeSpecific h u (OfferActivity a) = encodeOffer h u a
|
|
|
|
encodeSpecific h _ (PushActivity a) = encodePush h a
|
|
|
|
encodeSpecific _ _ (RejectActivity a) = encodeReject a
|
|
|
|
encodeSpecific _ _ (ResolveActivity a) = encodeResolve a
|
|
|
|
encodeSpecific h _ (UndoActivity a) = encodeUndo h a
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2020-05-02 02:48:01 +09:00
|
|
|
emptyAudience :: Audience u
|
|
|
|
emptyAudience = Audience [] [] [] [] [] []
|
|
|
|
|
2020-02-03 23:53:12 +09:00
|
|
|
emptyActivity :: Activity u
|
|
|
|
emptyActivity = Activity
|
2022-06-22 16:39:38 +09:00
|
|
|
{ activityId = Nothing
|
|
|
|
, activityActor = topLocalURI
|
|
|
|
, activityCapability = Nothing
|
|
|
|
, activitySummary = Nothing
|
|
|
|
, activityAudience = emptyAudience
|
2022-08-16 23:55:21 +09:00
|
|
|
, activityFulfills = []
|
2022-06-22 16:39:38 +09:00
|
|
|
, activitySpecific =
|
2020-02-03 23:53:12 +09:00
|
|
|
RejectActivity $ Reject $ ObjURI (Authority "" Nothing) topLocalURI
|
|
|
|
}
|
|
|
|
|
2019-01-22 00:54:57 +09:00
|
|
|
typeActivityStreams2 :: ContentType
|
|
|
|
typeActivityStreams2 = "application/activity+json"
|
|
|
|
|
|
|
|
typeActivityStreams2LD :: ContentType
|
|
|
|
typeActivityStreams2LD =
|
|
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
|
|
|
|
2019-02-07 19:34:33 +09:00
|
|
|
hActivityPubActor :: HeaderName
|
|
|
|
hActivityPubActor = "ActivityPub-Actor"
|
|
|
|
|
2019-03-20 21:01:10 +09:00
|
|
|
provideAP :: (Monad m, ToJSON a) => m a -> Writer (Endo [ProvidedRep m]) ()
|
|
|
|
provideAP mk =
|
|
|
|
-- let enc = toEncoding v
|
2019-01-22 00:54:57 +09:00
|
|
|
-- provideRepType typeActivityStreams2 $ return enc
|
2019-03-20 21:01:10 +09:00
|
|
|
provideRepType typeActivityStreams2LD $ toEncoding <$> mk
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-07-01 01:53:53 +09:00
|
|
|
provideAP' :: Monad m => m ByteString -> Writer (Endo [ProvidedRep m]) ()
|
2019-06-29 08:15:08 +09:00
|
|
|
provideAP' = provideRepType typeActivityStreams2LD
|
|
|
|
|
2019-01-22 00:54:57 +09:00
|
|
|
data APGetError
|
|
|
|
= APGetErrorHTTP HttpException
|
|
|
|
| APGetErrorJSON JSONException
|
2019-01-22 07:24:09 +09:00
|
|
|
| APGetErrorContentType Text
|
2019-01-22 00:54:57 +09:00
|
|
|
deriving Show
|
|
|
|
|
|
|
|
instance Exception APGetError
|
|
|
|
|
|
|
|
-- | Perform an HTTP GET request to fetch an ActivityPub object.
|
|
|
|
--
|
|
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
|
|
-- * Set _Accept_ request header
|
|
|
|
-- * Perform the GET request
|
|
|
|
-- * Verify the _Content-Type_ response header
|
|
|
|
-- * Parse the JSON response body
|
|
|
|
httpGetAP
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u, FromJSON a)
|
2019-01-22 00:54:57 +09:00
|
|
|
=> Manager
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Either (ObjURI u) (SubURI u)
|
2019-01-22 00:54:57 +09:00
|
|
|
-> m (Either APGetError (Response a))
|
|
|
|
httpGetAP manager uri =
|
2019-02-08 08:08:28 +09:00
|
|
|
liftIO $
|
|
|
|
mkResult <$> try (httpAPEither manager =<< requestFromURI (toURI uri))
|
2019-01-22 00:54:57 +09:00
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
toURI = either uriFromObjURI uriFromSubURI
|
2019-01-22 00:54:57 +09:00
|
|
|
lookup' x = map snd . filter ((== x) . fst)
|
|
|
|
mkResult (Left e) = Left $ APGetErrorHTTP e
|
|
|
|
mkResult (Right r) =
|
|
|
|
case lookup' hContentType $ responseHeaders r of
|
|
|
|
[] -> Left $ APGetErrorContentType "No Content-Type"
|
|
|
|
[b] -> if b == typeActivityStreams2LD || b == typeActivityStreams2
|
|
|
|
then case responseBody r of
|
|
|
|
Left e -> Left $ APGetErrorJSON e
|
|
|
|
Right v -> Right $ v <$ r
|
2019-01-22 07:24:09 +09:00
|
|
|
else Left $ APGetErrorContentType $ "Non-AP Content-Type: " <> decodeUtf8 b
|
2019-01-22 00:54:57 +09:00
|
|
|
_ -> Left $ APGetErrorContentType "Multiple Content-Type"
|
|
|
|
|
2019-03-05 17:26:41 +09:00
|
|
|
data APPostError
|
2019-03-11 08:15:42 +09:00
|
|
|
= APPostErrorSig S.HttpSigGenError
|
2019-03-05 17:26:41 +09:00
|
|
|
| APPostErrorHTTP HttpException
|
|
|
|
deriving Show
|
|
|
|
|
|
|
|
instance Exception APPostError
|
|
|
|
|
2019-04-28 19:18:50 +09:00
|
|
|
hActivityPubForwarder :: HeaderName
|
|
|
|
hActivityPubForwarder = "ActivityPub-Forwarder"
|
2019-04-26 09:25:50 +09:00
|
|
|
|
2019-04-28 19:18:50 +09:00
|
|
|
hForwardingSignature :: HeaderName
|
|
|
|
hForwardingSignature = "Forwarding-Signature"
|
2019-04-26 09:25:50 +09:00
|
|
|
|
2019-04-28 19:18:50 +09:00
|
|
|
hForwardedSignature :: HeaderName
|
|
|
|
hForwardedSignature = "Forwarded-Signature"
|
2019-04-26 09:25:50 +09:00
|
|
|
|
2019-01-22 00:54:57 +09:00
|
|
|
-- | Perform an HTTP POST request to submit an ActivityPub object.
|
|
|
|
--
|
|
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
|
|
-- * Set _Content-Type_ request header
|
2019-02-07 19:34:33 +09:00
|
|
|
-- * Set _ActivityPub-Actor_ request header
|
2019-04-26 00:49:15 +09:00
|
|
|
-- * Set _Digest_ request header using SHA-256 hash
|
2019-04-28 19:18:50 +09:00
|
|
|
-- * If recipient is given, set _ActivityPub-Forwarder_ header and compute
|
|
|
|
-- _Forwarding-Signature_ header
|
|
|
|
-- * If forwarded signature is given, set set _ActivityPub-Forwarder_ and
|
|
|
|
-- _Forwarded-Signature_ headers
|
2019-01-22 00:54:57 +09:00
|
|
|
-- * Compute HTTP signature and add _Signature_ request header
|
|
|
|
-- * Perform the POST request
|
|
|
|
-- * Verify the response status is 2xx
|
|
|
|
httpPostAP
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u, ToJSON a)
|
2019-01-22 00:54:57 +09:00
|
|
|
=> Manager
|
2019-07-23 22:59:48 +09:00
|
|
|
-> ObjURI u
|
2019-01-22 00:54:57 +09:00
|
|
|
-> NonEmpty HeaderName
|
2019-04-26 09:25:50 +09:00
|
|
|
-> S.KeyId
|
|
|
|
-> (ByteString -> S.Signature)
|
2019-02-07 19:34:33 +09:00
|
|
|
-> Text
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Maybe (Either (ObjURI u) ByteString)
|
2019-01-22 00:54:57 +09:00
|
|
|
-> a
|
2019-03-05 17:26:41 +09:00
|
|
|
-> m (Either APPostError (Response ()))
|
2019-04-28 19:18:50 +09:00
|
|
|
httpPostAP manager uri headers keyid sign uSender mfwd value =
|
2019-05-04 06:04:53 +09:00
|
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd $ encode value
|
|
|
|
|
|
|
|
-- | Like 'httpPostAP', except it takes the object as a raw lazy
|
|
|
|
-- 'BL.ByteString'. It's your responsibility to make sure it's valid JSON.
|
|
|
|
httpPostAPBytes
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u)
|
2019-05-04 06:04:53 +09:00
|
|
|
=> Manager
|
2019-07-23 22:59:48 +09:00
|
|
|
-> ObjURI u
|
2019-05-04 06:04:53 +09:00
|
|
|
-> NonEmpty HeaderName
|
|
|
|
-> S.KeyId
|
|
|
|
-> (ByteString -> S.Signature)
|
|
|
|
-> Text
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Maybe (Either (ObjURI u) ByteString)
|
2019-05-04 06:04:53 +09:00
|
|
|
-> BL.ByteString
|
|
|
|
-> m (Either APPostError (Response ()))
|
|
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd body =
|
2019-04-28 19:18:50 +09:00
|
|
|
liftIO $ runExceptT $ do
|
2019-07-23 22:59:48 +09:00
|
|
|
req <- requestFromURI $ uriFromObjURI uri
|
2019-05-04 06:04:53 +09:00
|
|
|
let digest = formatHttpBodyDigest SHA256 "SHA-256" $ hashlazy body
|
2019-04-28 19:18:50 +09:00
|
|
|
req' =
|
|
|
|
setRequestCheckStatus $
|
|
|
|
consHeader hContentType typeActivityStreams2LD $
|
|
|
|
consHeader hActivityPubActor (encodeUtf8 uSender) $
|
|
|
|
consHeader hDigest digest $
|
|
|
|
req { method = "POST"
|
|
|
|
, requestBody = RequestBodyLBS body
|
|
|
|
}
|
|
|
|
req'' <- tryExceptT APPostErrorSig $ signRequest headers Nothing keyid sign Nothing req'
|
|
|
|
req''' <-
|
|
|
|
case mfwd of
|
|
|
|
Nothing -> return req''
|
|
|
|
Just (Left uRecip) ->
|
|
|
|
tryExceptT APPostErrorSig $
|
2019-07-23 22:59:48 +09:00
|
|
|
signRequestInto hForwardingSignature (hDigest :| [hActivityPubForwarder]) Nothing keyid sign Nothing $ consHeader hActivityPubForwarder (encodeUtf8 $ renderObjURI uRecip) req''
|
2019-04-28 19:18:50 +09:00
|
|
|
Just (Right sig) ->
|
|
|
|
return $
|
|
|
|
consHeader hForwardedSignature sig $
|
|
|
|
consHeader hActivityPubForwarder (encodeUtf8 uSender)
|
|
|
|
req''
|
|
|
|
tryExceptT APPostErrorHTTP $ httpNoBody req''' manager
|
2019-01-22 00:54:57 +09:00
|
|
|
where
|
|
|
|
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
2019-04-28 19:18:50 +09:00
|
|
|
tryExceptT adapt action = ExceptT $ first adapt <$> try action
|
2019-02-04 08:39:56 +09:00
|
|
|
|
2019-02-06 11:48:23 +09:00
|
|
|
-- | Result of GETing the keyId URI and processing the JSON document.
|
|
|
|
data Fetched = Fetched
|
2020-04-11 23:58:38 +09:00
|
|
|
{ fetchedPublicKey :: PublicVerifKey
|
2019-03-11 08:15:42 +09:00
|
|
|
-- ^ The Ed25519 or RSA public key corresponding to the URI we requested.
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedKeyExpires :: Maybe UTCTime
|
2019-02-06 11:48:23 +09:00
|
|
|
-- ^ Optional expiration time declared for the key we received.
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedActorId :: LocalURI
|
2019-02-06 11:48:23 +09:00
|
|
|
-- ^ The @id URI of the actor for whom the key's signature applies.
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedActorName :: Maybe Text
|
2019-05-21 17:44:11 +09:00
|
|
|
-- ^ Name of the actor for whom the key's signature applies.
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedActorInbox :: LocalURI
|
2019-02-15 08:27:40 +09:00
|
|
|
-- ^ The inbox URI of the actor for whom the key's signature applies.
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedActorFollowers :: Maybe LocalURI
|
|
|
|
-- ^ The follower collection URI of the actor for whom the key's
|
|
|
|
-- signature applies.
|
|
|
|
, fetchedKeyShared :: Bool
|
2019-02-06 11:48:23 +09:00
|
|
|
-- ^ Whether the key we received is shared. A shared key can sign
|
|
|
|
-- requests for any actor on the same instance, while a personal key is
|
|
|
|
-- only for one actor. Knowing whether the key is shared will allow us
|
|
|
|
-- when receiving more requests, whether to accept signatures made on
|
|
|
|
-- different actors, or allow only a single permanent actor for the key
|
|
|
|
-- we received.
|
|
|
|
}
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchAP' :: (MonadIO m, UriMode u, FromJSON a) => Manager -> Either (ObjURI u) (SubURI u) -> ExceptT APGetError m a
|
2019-04-16 23:27:50 +09:00
|
|
|
fetchAP' m u = ExceptT $ second responseBody <$> httpGetAP m u
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchAP :: (MonadIO m, UriMode u, FromJSON a) => Manager -> Either (ObjURI u) (SubURI u) -> ExceptT String m a
|
2019-04-16 23:27:50 +09:00
|
|
|
fetchAP m u = withExceptT displayException $ fetchAP' m u
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-02-24 02:17:52 +09:00
|
|
|
{-
|
2019-02-22 08:59:53 +09:00
|
|
|
fetchAPH :: (MonadIO m, ActivityPub a) => Manager -> Text -> LocalURI -> ExceptT String m a
|
|
|
|
fetchAPH m h lu = do
|
|
|
|
Doc h' v <- fetchAP m $ l2f h lu
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else throwE "Object @id URI's host doesn't match the URI we fetched"
|
2019-02-24 02:17:52 +09:00
|
|
|
-}
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchAPID' :: (MonadIO m, UriMode u, ActivityPub a) => Manager -> (a u -> LocalURI) -> Authority u -> LocalURI -> m (Either (Maybe APGetError) (a u))
|
2019-04-16 23:27:50 +09:00
|
|
|
fetchAPID' m getId h lu = runExceptT $ do
|
2019-07-23 22:59:48 +09:00
|
|
|
Doc h' v <- withExceptT Just $ fetchAP' m $ Left $ ObjURI h lu
|
2019-02-22 08:59:53 +09:00
|
|
|
if h == h' && getId v == lu
|
|
|
|
then return v
|
2019-04-16 23:27:50 +09:00
|
|
|
else throwE Nothing
|
|
|
|
|
2022-09-21 21:50:26 +09:00
|
|
|
fetchTip :: (MonadIO m, UriMode u) => Manager -> Authority u -> LocalURI -> m (Either (Maybe APGetError) (Either (Repo u) (Branch u)))
|
|
|
|
fetchTip m h lu = runExceptT $ do
|
|
|
|
tip <- fmap toEither $ withExceptT Just $ fetchAP' m $ Left $ ObjURI h lu
|
|
|
|
bitraverse
|
|
|
|
(\ (Doc h' repo) ->
|
|
|
|
if h == h' && actorId (actorLocal $ repoActor repo) == lu
|
|
|
|
then return repo
|
|
|
|
else throwE Nothing
|
|
|
|
)
|
|
|
|
(\ (Doc _ branch) -> pure branch)
|
|
|
|
tip
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchRecipient :: (MonadIO m, UriMode u) => Manager -> Authority u -> LocalURI -> m (Either (Maybe APGetError) (Recipient u))
|
2019-05-18 07:42:01 +09:00
|
|
|
fetchRecipient m = fetchAPID' m getId
|
|
|
|
where
|
2022-07-25 01:52:28 +09:00
|
|
|
getId (RecipientActor a) = actorId $ actorLocal a
|
2019-05-18 07:42:01 +09:00
|
|
|
getId (RecipientCollection c) = collectionId c
|
|
|
|
|
2022-08-28 22:51:43 +09:00
|
|
|
fetchResource :: (MonadIO m, UriMode u) => Manager -> Authority u -> LocalURI -> m (Either (Maybe APGetError) (Resource u))
|
|
|
|
fetchResource m = fetchAPID' m getId
|
|
|
|
where
|
|
|
|
getId (ResourceActor a) = actorId $ actorLocal a
|
|
|
|
getId (ResourceChild luId _) = luId
|
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchAPID :: (MonadIO m, UriMode u, ActivityPub a) => Manager -> (a u -> LocalURI) -> Authority u -> LocalURI -> m (Either String (a u))
|
2019-04-16 23:27:50 +09:00
|
|
|
fetchAPID m getId h lu = first showError <$> fetchAPID' m getId h lu
|
|
|
|
where
|
|
|
|
showError Nothing = "Object @id doesn't match the URI we fetched"
|
|
|
|
showError (Just e) = displayException e
|
2019-02-22 08:59:53 +09:00
|
|
|
|
2019-04-17 01:10:17 +09:00
|
|
|
data FetchAPError
|
|
|
|
= FetchAPErrorGet APGetError
|
|
|
|
-- Object @id doesn't match the URI we fetched
|
|
|
|
| FetchAPErrorIdMismatch
|
|
|
|
-- Object @id URI's host doesn't match the URI we fetched
|
|
|
|
| FetchAPErrorHostMismatch
|
|
|
|
deriving Show
|
|
|
|
|
|
|
|
fetchAPIDOrH'
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u, ActivityPub a, ActivityPub b)
|
2019-02-22 08:59:53 +09:00
|
|
|
=> Manager
|
2019-07-23 22:59:48 +09:00
|
|
|
-> (a u -> LocalRefURI)
|
|
|
|
-> Authority u
|
|
|
|
-> LocalRefURI
|
|
|
|
-> ExceptT FetchAPError m (Either (a u) (b u))
|
|
|
|
fetchAPIDOrH' m getId h (LocalRefURI lu) = do
|
|
|
|
e <- withExceptT FetchAPErrorGet $ fetchAP' m $ bimap (ObjURI h) (SubURI h) lu
|
2019-02-22 08:59:53 +09:00
|
|
|
case e of
|
|
|
|
Left' (Doc h' x) ->
|
2019-07-23 22:59:48 +09:00
|
|
|
if h == h' && getId x == LocalRefURI lu
|
2019-02-22 08:59:53 +09:00
|
|
|
then return $ Left x
|
2019-04-17 01:10:17 +09:00
|
|
|
else throwE FetchAPErrorIdMismatch
|
2019-02-22 08:59:53 +09:00
|
|
|
Right' (Doc h' y) ->
|
|
|
|
if h == h'
|
|
|
|
then return $ Right y
|
2019-04-17 01:10:17 +09:00
|
|
|
else throwE FetchAPErrorHostMismatch
|
|
|
|
|
|
|
|
fetchAPIDOrH
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u, ActivityPub a, ActivityPub b)
|
2019-04-17 01:10:17 +09:00
|
|
|
=> Manager
|
2019-07-23 22:59:48 +09:00
|
|
|
-> (a u -> LocalRefURI)
|
|
|
|
-> Authority u
|
|
|
|
-> LocalRefURI
|
|
|
|
-> ExceptT String m (Either (a u) (b u))
|
2019-04-17 01:10:17 +09:00
|
|
|
fetchAPIDOrH m getId h lu = withExceptT show $ fetchAPIDOrH' m getId h lu
|
2019-02-22 08:59:53 +09:00
|
|
|
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 18:20:13 +09:00
|
|
|
-- | Fetches the given actor and checks whether it lists the given key (as a
|
|
|
|
-- URI, not as an embedded object). If it does, returns 'Right' the fetched
|
|
|
|
-- actor. Otherwise, or if an error occurs during fetching, returns 'Left' an
|
|
|
|
-- error message.
|
2019-07-23 22:59:48 +09:00
|
|
|
keyListedByActor
|
|
|
|
:: (MonadIO m, UriMode u)
|
|
|
|
=> Manager
|
|
|
|
-> Authority u
|
|
|
|
-> LocalRefURI
|
|
|
|
-> LocalURI
|
|
|
|
-> m (Either String (Actor u))
|
2019-02-22 08:59:53 +09:00
|
|
|
keyListedByActor manager host luKey luActor = runExceptT $ do
|
2022-07-25 01:52:28 +09:00
|
|
|
actor <- ExceptT $ fetchAPID manager (actorId . actorLocal) host luActor
|
2019-02-22 08:59:53 +09:00
|
|
|
if keyUriListed luKey actor
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 18:20:13 +09:00
|
|
|
then return actor
|
|
|
|
else throwE "Actor publicKey has no URI matching pkey @id"
|
|
|
|
where
|
2019-07-23 22:59:48 +09:00
|
|
|
keyUriListed (LocalRefURI uk) a =
|
|
|
|
let match (Left uri) = Left uri == uk
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 18:20:13 +09:00
|
|
|
match (Right _) = False
|
2022-07-25 01:52:28 +09:00
|
|
|
in any match $ actorPublicKeys $ actorLocal a
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 18:20:13 +09:00
|
|
|
|
2019-07-23 22:59:48 +09:00
|
|
|
matchKeyObj
|
|
|
|
:: (Foldable f, Monad m, UriMode u)
|
|
|
|
=> LocalRefURI
|
|
|
|
-> f (Either LocalURI (PublicKey u))
|
|
|
|
-> ExceptT String m (PublicKey u)
|
2019-02-24 10:21:42 +09:00
|
|
|
matchKeyObj luKey es =
|
|
|
|
case find' (match luKey) es of
|
|
|
|
Nothing -> throwE "keyId resolved to actor which doesn't have a key object with that ID"
|
|
|
|
Just pk -> return pk
|
2019-02-24 02:17:52 +09:00
|
|
|
where
|
2019-02-24 10:21:42 +09:00
|
|
|
find' :: Foldable f => (a -> Maybe b) -> f a -> Maybe b
|
|
|
|
find' p = join . fmap getFirst . foldMap (Just . First . p)
|
2019-02-24 02:17:52 +09:00
|
|
|
match _ (Left _) = Nothing
|
|
|
|
match luk (Right pk) =
|
|
|
|
if publicKeyId pk == luk
|
|
|
|
then Just pk
|
|
|
|
else Nothing
|
|
|
|
|
2019-03-11 08:15:42 +09:00
|
|
|
verifyAlgo :: Maybe S.Algorithm -> PublicVerifKey -> Either String ()
|
|
|
|
verifyAlgo Nothing _ = Right ()
|
|
|
|
verifyAlgo (Just a) k =
|
|
|
|
case a of
|
|
|
|
S.AlgorithmEd25519 ->
|
|
|
|
case k of
|
|
|
|
PublicVerifKeyEd25519 _ -> Right ()
|
|
|
|
PublicVerifKeyRSA _ ->
|
|
|
|
Left "Algo mismatch, algo is Ed25519 but actual key is RSA"
|
|
|
|
S.AlgorithmRsaSha256 ->
|
|
|
|
case k of
|
|
|
|
PublicVerifKeyEd25519 _ ->
|
|
|
|
Left
|
|
|
|
"Algo mismatch, algo is RSA-SHA256 but actual key is \
|
|
|
|
\Ed25519"
|
|
|
|
PublicVerifKeyRSA _ -> Right ()
|
|
|
|
S.AlgorithmOther b -> Left $ concat
|
|
|
|
[ "Unrecognized algo "
|
|
|
|
, BC.unpack b
|
|
|
|
, ", actual key is "
|
|
|
|
, case k of
|
|
|
|
PublicVerifKeyEd25519 _ -> "Ed25519"
|
|
|
|
PublicVerifKeyRSA _ -> "RSA"
|
|
|
|
]
|
2019-02-24 02:17:52 +09:00
|
|
|
|
|
|
|
-- | Fetch a key we don't have cached locally.
|
|
|
|
fetchUnknownKey
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u)
|
2019-02-04 08:39:56 +09:00
|
|
|
=> Manager
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 08:15:42 +09:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Authority u
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Instance host
|
2019-02-22 08:59:53 +09:00
|
|
|
-> Maybe LocalURI
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Actor URI possibly provided in the HTTP request's actor header
|
2019-07-23 22:59:48 +09:00
|
|
|
-> LocalRefURI
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Key URI provided in HTTP signature header
|
|
|
|
-> ExceptT String m Fetched
|
2019-03-11 08:15:42 +09:00
|
|
|
fetchUnknownKey manager malgo host mluActor luKey = do
|
2019-02-22 08:59:53 +09:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
2019-03-11 08:15:42 +09:00
|
|
|
fetched <-
|
2019-02-04 08:39:56 +09:00
|
|
|
case obj of
|
2019-02-22 08:59:53 +09:00
|
|
|
Left pkey -> do
|
2019-02-22 16:20:19 +09:00
|
|
|
(oi, luActor) <-
|
2019-02-22 08:59:53 +09:00
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance ->
|
|
|
|
case mluActor of
|
|
|
|
Nothing -> throwE "Key is shared but actor header not specified!"
|
2019-02-22 16:20:19 +09:00
|
|
|
Just u -> return (True, u)
|
2019-02-22 08:59:53 +09:00
|
|
|
OwnerActor owner -> do
|
|
|
|
for_ mluActor $ \ lu ->
|
|
|
|
if owner == lu
|
2019-02-17 09:14:05 +09:00
|
|
|
then return ()
|
|
|
|
else throwE "Key's owner doesn't match actor header"
|
2019-02-22 16:20:19 +09:00
|
|
|
return (False, owner)
|
2022-07-25 01:52:28 +09:00
|
|
|
Actor local detail <- ExceptT $ keyListedByActor manager host luKey luActor
|
2019-03-11 08:15:42 +09:00
|
|
|
return Fetched
|
2020-04-11 23:58:38 +09:00
|
|
|
{ fetchedPublicKey = publicKeyMaterial pkey
|
|
|
|
, fetchedKeyExpires = publicKeyExpires pkey
|
|
|
|
, fetchedActorId = luActor
|
2022-07-25 01:52:28 +09:00
|
|
|
, fetchedActorName = actorName detail <|> actorUsername detail
|
|
|
|
, fetchedActorInbox = actorInbox local
|
|
|
|
, fetchedActorFollowers = actorFollowers local
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedKeyShared = oi
|
2019-03-11 08:15:42 +09:00
|
|
|
}
|
2022-07-25 01:52:28 +09:00
|
|
|
Right (Actor local detail) -> do
|
2019-07-23 22:59:48 +09:00
|
|
|
case luKey of
|
|
|
|
LocalRefURI (Right lsu) |
|
2022-07-25 01:52:28 +09:00
|
|
|
actorId local == localSubUriResource lsu -> return ()
|
2019-07-23 22:59:48 +09:00
|
|
|
_ -> throwE "Actor ID doesn't match the keyid URI we fetched"
|
2019-02-22 08:59:53 +09:00
|
|
|
for_ mluActor $ \ lu ->
|
2022-07-25 01:52:28 +09:00
|
|
|
if actorId local == lu
|
2019-02-17 09:14:05 +09:00
|
|
|
then return ()
|
|
|
|
else throwE "Key's owner doesn't match actor header"
|
2022-07-25 01:52:28 +09:00
|
|
|
pk <- matchKeyObj luKey $ actorPublicKeys local
|
2019-02-22 17:30:43 +09:00
|
|
|
owner <- case publicKeyOwner pk of
|
|
|
|
OwnerInstance -> throwE "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
|
2019-02-24 10:21:42 +09:00
|
|
|
OwnerActor owner ->
|
2022-07-25 01:52:28 +09:00
|
|
|
if owner == actorId local
|
2019-02-22 17:30:43 +09:00
|
|
|
then return owner
|
|
|
|
else throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
2019-03-11 08:15:42 +09:00
|
|
|
return Fetched
|
2020-04-11 23:58:38 +09:00
|
|
|
{ fetchedPublicKey = publicKeyMaterial pk
|
|
|
|
, fetchedKeyExpires = publicKeyExpires pk
|
|
|
|
, fetchedActorId = owner
|
2022-07-25 01:52:28 +09:00
|
|
|
, fetchedActorName = actorName detail <|> actorUsername detail
|
|
|
|
, fetchedActorInbox = actorInbox local
|
|
|
|
, fetchedActorFollowers = actorFollowers local
|
2020-04-11 23:58:38 +09:00
|
|
|
, fetchedKeyShared = False
|
2019-03-11 08:15:42 +09:00
|
|
|
}
|
|
|
|
ExceptT . pure $ verifyAlgo malgo $ fetchedPublicKey fetched
|
|
|
|
return fetched
|
|
|
|
|
|
|
|
keyDetail pk = (publicKeyMaterial pk, publicKeyExpires pk)
|
2019-02-24 02:17:52 +09:00
|
|
|
|
|
|
|
-- | Fetch a personal key we already have cached locally, but we'd like to
|
|
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
|
|
fetchKnownPersonalKey
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u)
|
2019-02-24 02:17:52 +09:00
|
|
|
=> Manager
|
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 08:15:42 +09:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Authority u
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Instance host
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Key owner actor ID URI
|
2019-07-23 22:59:48 +09:00
|
|
|
-> LocalRefURI
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Key URI
|
2019-03-11 08:15:42 +09:00
|
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
2019-07-23 22:59:48 +09:00
|
|
|
fetchKnownPersonalKey manager malgo host luOwner luKey@(LocalRefURI ek) = do
|
2019-02-24 02:17:52 +09:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
2019-03-11 08:15:42 +09:00
|
|
|
(material, mexpires) <-
|
2019-02-24 02:17:52 +09:00
|
|
|
case obj of
|
|
|
|
Left pkey -> do
|
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
|
|
OwnerActor owner ->
|
|
|
|
when (luOwner /= owner) $ throwE "Key owner changed"
|
|
|
|
return $ keyDetail pkey
|
2022-07-25 01:52:28 +09:00
|
|
|
Right (Actor local detail) -> do
|
|
|
|
unless (Right (actorId local) == second localSubUriResource ek) $
|
2019-02-24 02:17:52 +09:00
|
|
|
throwE "Actor ID doesn't match the keyid URI we fetched"
|
2022-07-25 01:52:28 +09:00
|
|
|
unless (actorId local == luOwner) $
|
2019-02-24 02:17:52 +09:00
|
|
|
throwE "Key owner changed"
|
2022-07-25 01:52:28 +09:00
|
|
|
pk <- matchKeyObj luKey $ actorPublicKeys local
|
2019-02-24 02:17:52 +09:00
|
|
|
case publicKeyOwner pk of
|
|
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
|
|
OwnerActor owner ->
|
|
|
|
when (owner /= luOwner) $
|
|
|
|
throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
|
|
|
return $ keyDetail pk
|
2019-03-11 08:15:42 +09:00
|
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
|
|
return (material, mexpires)
|
2019-02-24 02:17:52 +09:00
|
|
|
|
|
|
|
-- | Fetch a shared key we already have cached locally, but we'd like to
|
|
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
|
|
fetchKnownSharedKey
|
2019-07-23 22:59:48 +09:00
|
|
|
:: (MonadIO m, UriMode u)
|
2019-02-24 02:17:52 +09:00
|
|
|
=> Manager
|
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 08:15:42 +09:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-07-23 22:59:48 +09:00
|
|
|
-> Authority u
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Instance host
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Actor ID from HTTP actor header
|
2019-07-23 22:59:48 +09:00
|
|
|
-> LocalRefURI
|
2019-02-24 02:17:52 +09:00
|
|
|
-- ^ Key URI
|
2019-03-11 08:15:42 +09:00
|
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
2019-03-22 06:38:59 +09:00
|
|
|
fetchKnownSharedKey manager malgo host luActor luKey = do
|
2019-02-24 02:17:52 +09:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
|
|
|
pkey <-
|
2019-07-23 22:59:48 +09:00
|
|
|
case asKeyOrActor host obj of
|
2019-02-24 02:17:52 +09:00
|
|
|
Left pk -> return pk
|
|
|
|
Right _actor -> throwE "Expected stand-alone key, got embedded key"
|
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance -> return ()
|
|
|
|
OwnerActor _owner -> throwE "Shared key became personal"
|
2019-03-11 08:15:42 +09:00
|
|
|
let (material, mexpires) = keyDetail pkey
|
|
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
|
|
return (material, mexpires)
|
2019-07-23 22:59:48 +09:00
|
|
|
where
|
|
|
|
asKeyOrActor
|
|
|
|
:: Authority u
|
|
|
|
-> Either (PublicKey u) (Actor u)
|
|
|
|
-> Either (PublicKey u) (Actor u)
|
|
|
|
asKeyOrActor _ = id
|
2020-06-18 19:38:04 +09:00
|
|
|
|
|
|
|
data Obj u = Obj
|
|
|
|
{ objId :: ObjURI u
|
|
|
|
, objType :: Text
|
|
|
|
|
|
|
|
, objContext :: Maybe (ObjURI u)
|
|
|
|
, objFollowers :: Maybe LocalURI
|
|
|
|
, objInbox :: Maybe LocalURI
|
|
|
|
, objTeam :: Maybe LocalURI
|
|
|
|
}
|
|
|
|
|
|
|
|
instance UriMode u => FromJSON (Obj u) where
|
|
|
|
parseJSON = withObject "Obj" $ \ o -> do
|
|
|
|
id_@(ObjURI h _) <- o .: "id" <|> o .: "@id"
|
|
|
|
Obj id_
|
|
|
|
<$> (o .: "type" <|> o .: "@type")
|
|
|
|
<*> o .:? "context"
|
|
|
|
<*> withAuthorityMaybeO h (o .:? "followers")
|
|
|
|
<*> withAuthorityMaybeO h (o .:? "inbox")
|
|
|
|
<*> withAuthorityMaybeO h (o .:? "team")
|