packages feed

scuttlebutt-types 0.1.0 → 0.2.0

raw patch · 10 files changed

+276/−83 lines, 10 filesdep +cryptonitedep +memorydep −bytestring-conversionPVP ok

version bump matches the API change (PVP)

Dependencies added: cryptonite, memory

Dependencies removed: bytestring-conversion

API changes (from Hackage documentation)

- Ssb.Types.Key: instance Data.Aeson.Types.FromJSON.FromJSON Ssb.Types.Key.PublicKey
- Ssb.Types.Key: instance Data.Aeson.Types.ToJSON.ToJSON Ssb.Types.Key.PublicKey
- Ssb.Types.Key: instance GHC.Generics.Generic Ssb.Types.Key.PublicKey
- Ssb.Types.Key: parsePublicKey :: ByteString -> Maybe PublicKey
- Ssb.Types.Link: type BlobLink = String
- Ssb.Types.Link: type GenericLink = String
- Ssb.Types.Link: type MessageLink = String
- Ssb.Types.Message: type HashType = String
+ Ssb.Types.Hash: Hashed :: v -> Hash -> Hashed v
+ Ssb.Types.Hash: Sha256 :: Digest SHA256 -> Hash
+ Ssb.Types.Hash: [hashOf] :: Hashed v -> Hash
+ Ssb.Types.Hash: [hashedValue] :: Hashed v -> v
+ Ssb.Types.Hash: [sha256] :: Hash -> Digest SHA256
+ Ssb.Types.Hash: calcHashed :: ByteString -> (v -> HashType) -> (ByteString -> Maybe v) -> Maybe (Hashed v)
+ Ssb.Types.Hash: data Hashed v
+ Ssb.Types.Hash: formatHash :: Hash -> ByteString
+ Ssb.Types.Hash: instance GHC.Base.Functor Ssb.Types.Hash.Hashed
+ Ssb.Types.Hash: instance GHC.Classes.Eq Ssb.Types.Hash.Hash
+ Ssb.Types.Hash: instance GHC.Classes.Eq v => GHC.Classes.Eq (Ssb.Types.Hash.Hashed v)
+ Ssb.Types.Hash: instance GHC.Classes.Ord Ssb.Types.Hash.Hash
+ Ssb.Types.Hash: instance GHC.Classes.Ord v => GHC.Classes.Ord (Ssb.Types.Hash.Hashed v)
+ Ssb.Types.Hash: instance GHC.Show.Show Ssb.Types.Hash.Hash
+ Ssb.Types.Hash: instance GHC.Show.Show v => GHC.Show.Show (Ssb.Types.Hash.Hashed v)
+ Ssb.Types.Hash: newtype Hash
+ Ssb.Types.Hash: parseSha256 :: ByteString -> Maybe Hash
+ Ssb.Types.Hash: type HashType = Text
+ Ssb.Types.Link: BlobLink :: Hash -> BlobLink
+ Ssb.Types.Link: Link :: Char -> Text -> Text -> Link
+ Ssb.Types.Link: MessageLink :: Hash -> MessageLink
+ Ssb.Types.Link: [linkSigil] :: Link -> Char
+ Ssb.Types.Link: [linkTag] :: Link -> Text
+ Ssb.Types.Link: [linkTo] :: Link -> Text
+ Ssb.Types.Link: [unBlobLink] :: BlobLink -> Hash
+ Ssb.Types.Link: [unMessageLink] :: MessageLink -> Hash
+ Ssb.Types.Link: class IsLink t where toJSONLink = toJSON . toLink fromJSONLink o = do { l <- parseJSON o; either fail pure (fromLink l) }
+ Ssb.Types.Link: data Link
+ Ssb.Types.Link: formatLink :: Link -> Text
+ Ssb.Types.Link: fromJSONLink :: IsLink t => Value -> Parser t
+ Ssb.Types.Link: fromLink :: IsLink t => Link -> Either String t
+ Ssb.Types.Link: instance Data.Aeson.Types.FromJSON.FromJSON Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance Data.Aeson.Types.FromJSON.FromJSON Ssb.Types.Link.Link
+ Ssb.Types.Link: instance Data.Aeson.Types.FromJSON.FromJSON Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: instance Data.Aeson.Types.ToJSON.ToJSON Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance Data.Aeson.Types.ToJSON.ToJSON Ssb.Types.Link.Link
+ Ssb.Types.Link: instance Data.Aeson.Types.ToJSON.ToJSON Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: instance GHC.Classes.Eq Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance GHC.Classes.Eq Ssb.Types.Link.Link
+ Ssb.Types.Link: instance GHC.Classes.Eq Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: instance GHC.Classes.Ord Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance GHC.Classes.Ord Ssb.Types.Link.Link
+ Ssb.Types.Link: instance GHC.Classes.Ord Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: instance GHC.Show.Show Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance GHC.Show.Show Ssb.Types.Link.Link
+ Ssb.Types.Link: instance GHC.Show.Show Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: instance Ssb.Types.Link.IsLink Ssb.Types.Link.BlobLink
+ Ssb.Types.Link: instance Ssb.Types.Link.IsLink Ssb.Types.Link.FeedLink
+ Ssb.Types.Link: instance Ssb.Types.Link.IsLink Ssb.Types.Link.MessageLink
+ Ssb.Types.Link: newtype BlobLink
+ Ssb.Types.Link: newtype MessageLink
+ Ssb.Types.Link: parseLink :: Text -> Either String Link
+ Ssb.Types.Link: toJSONLink :: IsLink t => t -> Value
+ Ssb.Types.Link: toLink :: IsLink t => t -> Link
+ Ssb.Types.Message: messageLink :: Hashed (Message a) -> MessageLink
+ Ssb.Types.Message: parseMessage :: FromJSON a => ByteString -> Maybe (Hashed (Message a))
- Ssb.Types.Message: About :: GenericLink -> Maybe Text -> Maybe AboutImage -> About
+ Ssb.Types.Message: About :: Link -> Maybe Text -> Maybe AboutImage -> About
- Ssb.Types.Message: Mention :: GenericLink -> Mention
+ Ssb.Types.Message: Mention :: Link -> Mention
- Ssb.Types.Message: Pub :: Text -> Int -> PublicKey -> Pub
+ Ssb.Types.Message: Pub :: Text -> Int -> FeedLink -> Pub
- Ssb.Types.Message: Vote :: GenericLink -> Int -> Maybe Text -> Vote
+ Ssb.Types.Message: Vote :: Link -> Int -> Maybe Text -> Vote
- Ssb.Types.Message: [about] :: About -> GenericLink
+ Ssb.Types.Message: [about] :: About -> Link
- Ssb.Types.Message: [mentionLink] :: Mention -> GenericLink
+ Ssb.Types.Message: [mentionLink] :: Mention -> Link
- Ssb.Types.Message: [pubKey] :: Pub -> PublicKey
+ Ssb.Types.Message: [pubKey] :: Pub -> FeedLink
- Ssb.Types.Message: [voteLink] :: Vote -> GenericLink
+ Ssb.Types.Message: [voteLink] :: Vote -> Link

Files

CHANGELOG view
@@ -1,3 +1,11 @@+scuttlebutt-types (0.2.0) unstable; urgency=medium++  * Improved Link types.+  * Added Hash type.+  * Some API changes.++ -- Joey Hess <id@joeyh.name>  Thu, 05 Oct 2017 17:40:48 -0400+ scuttlebutt-types (0.1.0) unstable; urgency=low    * First release, split off from haskell-scuttlebutt.
scuttlebutt-types.cabal view
@@ -1,5 +1,5 @@ Name: scuttlebutt-types-Version: 0.1.0+Version: 0.2.0 License: BSD3 License-File: LICENSE Author: bkil.hu, Peter Ferenc Hajdu, Joey Hess@@ -23,16 +23,18 @@   Exposed-Modules:     Ssb.Types     Ssb.Types.Message-    Ssb.Types.Key     Ssb.Types.Link+    Ssb.Types.Key+    Ssb.Types.Hash   Build-Depends:     base (>= 4.7 && < 5),     bytestring,-    bytestring-conversion,     base64-bytestring,     aeson,     ed25519,-    text+    cryptonite,+    text,+    memory   Default-Language: Haskell2010  Test-Suite scuttlebutt-types-test@@ -44,10 +46,13 @@     scuttlebutt-types,     hspec,     bytestring,-    aeson+    aeson,+    text   Other-Modules:     MessageSpec+    LinkSpec     KeySpec+    HashSpec   Ghc-Options: -Wall -fno-warn-tabs -threaded -rtsopts -with-rtsopts=-N   Default-Language: Haskell2010 
+ src/Ssb/Types/Hash.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE PackageImports, OverloadedStrings #-}++module Ssb.Types.Hash where++import Data.ByteString+import qualified Data.ByteString.Lazy as L+import Data.ByteArray (convert)+import qualified Data.ByteString.Base64 as B64+import qualified Data.Text as T+import "cryptonite" Crypto.Hash as Crypto++newtype Hash = Sha256 { sha256 :: Crypto.Digest Crypto.SHA256 }+	deriving (Eq, Ord)++instance Show Hash where+	show = show . formatHash ++-- | Decodes a SHA256 hash, which is base64 encoded.+parseSha256 :: ByteString -> Maybe Hash+parseSha256 b = Sha256 <$> either+	(const Nothing)+	Crypto.digestFromByteString+	(B64.decode b)++-- | Formats a hash as a base64 encoded string.+formatHash :: Hash -> ByteString+formatHash (Sha256 { sha256 = s }) = B64.encode (convert s)++type HashType = T.Text++-- | A value accompanied with the hash of the ByteString that it was+-- deserialized from.+data Hashed v = Hashed+	{ hashedValue :: v+	, hashOf :: Hash+	}+	deriving (Show, Eq, Ord)++instance Functor Hashed where+	fmap f h = h { hashedValue = f (hashedValue h) }++calcHashed :: L.ByteString -> (v -> HashType) -> (L.ByteString -> Maybe v) -> Maybe (Hashed v)+calcHashed b gettype f = do+	v <- f b+	case gettype v of+		"sha256" -> Just $ Hashed v $ Sha256 (hashlazy b)+		_ -> Nothing
src/Ssb/Types/Key.hs view
@@ -1,45 +1,17 @@-{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}--module Ssb.Types.Key (-	PublicKey(..),-	parsePublicKey,-	parseEd25519PublicKey,-	formatPublicKey,-) where+module Ssb.Types.Key where  import Data.ByteString-import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import Data.Text.Encoding (decodeUtf8) import qualified Data.Text as T import qualified Data.ByteString.Base64 as B64-import qualified Data.Aeson as Aeson import qualified Crypto.Sign.Ed25519 as Ed-import GHC.Generics-import Data.Monoid  newtype PublicKey = Ed25519PublicKey { ed25519Key :: Ed.PublicKey }-	deriving (Eq, Ord, Generic)--instance Aeson.FromJSON PublicKey where-	parseJSON = Aeson.withText "PublicKey" $ \t ->-		case parsePublicKey (encodeUtf8 t) of-			Just pk -> return pk-			Nothing -> fail ("public key decode failure: " ++ T.unpack t)--instance Aeson.ToJSON PublicKey where-	toJSON = Aeson.String . decodeUtf8 . formatPublicKey+	deriving (Eq, Ord)  instance Show PublicKey where 	show  = T.unpack . decodeUtf8 . formatPublicKey --- | Parses a SSB public key, which is base64 encoded, and has a prefix of--- "@" and a suffix specifying the type of the key.-parsePublicKey :: ByteString -> Maybe PublicKey-parsePublicKey b = do-	b' <- stripPrefix "@" b-	case stripSuffix ".ed25519" b' of-		Just b'' -> parseEd25519PublicKey b''-		Nothing -> Nothing- -- | Decodes a SSB Ed25519 public key, which is base64 encoded. parseEd25519PublicKey :: ByteString -> Maybe PublicKey parseEd25519PublicKey b = Ed25519PublicKey <$> maybeEd25519PublicKey@@ -50,8 +22,7 @@ 		(Just . Ed.PublicKey) 		decodedPublicKey --- | Formats a SSB public key to a base64 encoded string with a prefix of--- "@" and suffix specifying the type of the key.+-- | Formats a SSB public key as a base64 encoded string. formatPublicKey :: PublicKey -> ByteString formatPublicKey (Ed25519PublicKey { ed25519Key = k }) =-	"@" <> B64.encode (Ed.unPublicKey k) <> ".ed25519"+	B64.encode (Ed.unPublicKey k)
src/Ssb/Types/Link.hs view
@@ -1,26 +1,139 @@--- | TODO newtypes for everything+{-# LANGUAGE OverloadedStrings #-} -module Ssb.Types.Link where+module Ssb.Types.Link (+	Link(..),+	parseLink,+	formatLink,+	FeedLink(..),+	MessageLink(..),+	BlobLink(..),+	IsLink(..),+) where  import Ssb.Types.Key+import Ssb.Types.Hash -import Data.Aeson+import Data.Monoid+import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import qualified Data.Text as T+import qualified Crypto.Sign.Ed25519 as Ed --- | A link to a message.-type MessageLink = String+-- | A link to a message, feed, or blob.+data Link = Link+	{ linkSigil :: Char+	-- ^ sigil that starts the link, eg '@'+	, linkTo :: T.Text+	-- ^ thing being linked to (base64 encoded)+	, linkTag :: T.Text+	-- ^ hash or key algorithm of thing being linked to, eg "sha256"+	} deriving (Eq, Ord) +instance Show Link where+	show = T.unpack . formatLink++instance Aeson.FromJSON Link where+	parseJSON = Aeson.withText "Link" $ either fail pure . parseLink++instance Aeson.ToJSON Link where+	toJSON = Aeson.String . formatLink++-- | Parses a Link eg+-- "@LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=.ed25519"+parseLink :: T.Text -> Either String Link+parseLink t+	| T.null t = Left "empty link"+	| otherwise =+		let (sigil, rest) = T.splitAt 1 t+		    (linkto, linktag) = T.breakOnEnd "." rest+		in if T.null linktag+			then Left "missing linkTag"+			else if T.null linkto+				then Left "empty link"+				else Right $ Link+					{ linkSigil = T.head sigil+					, linkTo = T.init linkto+					, linkTag = linktag+					}++-- | Formats a Link.+formatLink :: Link -> T.Text+formatLink l = T.singleton (linkSigil l) <> linkTo l <> "." <> linkTag l++-- | Class of types that are links.+class IsLink t where+	fromLink :: Link -> Either String t++	toLink :: t -> Link++	toJSONLink :: t -> Aeson.Value+	toJSONLink = Aeson.toJSON . toLink++	fromJSONLink :: Aeson.Value -> Aeson.Parser t+	fromJSONLink o = do+		l <- Aeson.parseJSON o+		either fail pure (fromLink l)+ -- | A link to a feed. newtype FeedLink = FeedLink { unFeedLink :: PublicKey } 	deriving (Show, Eq, Ord) -instance FromJSON FeedLink where-	parseJSON d = FeedLink <$> parseJSON d+instance IsLink FeedLink where+	fromLink l+		| linkSigil l == '@' && linkTag l == "ed25519" =+			maybe (Left "ed25519 key parse failed")+				(Right . FeedLink)+				(parseEd25519PublicKey $ encodeUtf8 $ linkTo l)+		| otherwise = Left "wrong sigil for feed link"+	toLink fl = Link '@' linkto "ed25519"+	  where+		linkto = decodeUtf8 $ Ed.unPublicKey $ ed25519Key $ unFeedLink fl -instance ToJSON FeedLink where-	toJSON = toJSON . unFeedLink+instance Aeson.ToJSON FeedLink where+	toJSON = toJSONLink +instance Aeson.FromJSON FeedLink where+	parseJSON = fromJSONLink++-- | A link to a message.+newtype MessageLink = MessageLink { unMessageLink :: Hash }+	deriving (Show, Eq, Ord)++instance IsLink MessageLink where+	fromLink l+		| linkSigil l == '%' && linkTag l == "sha256" =+			maybe (Left "sha256 hash parse failed")+				(Right . MessageLink)+				(parseSha256 $ encodeUtf8 $ linkTo l)+		| otherwise = Left "wrong sigil for message link"+	toLink fl = Link '%' linkto "sha256"+	  where+		linkto = decodeUtf8 $ formatHash $ unMessageLink fl++instance Aeson.ToJSON MessageLink where+	toJSON = toJSONLink++instance Aeson.FromJSON MessageLink where+	parseJSON = fromJSONLink+ -- | A link to a blob.-type BlobLink = String+newtype BlobLink = BlobLink { unBlobLink :: Hash }+	deriving (Show, Eq, Ord) --- | A link to a message, feed, or blob.-type GenericLink = String+instance IsLink BlobLink where+	fromLink l+		| linkSigil l == '&' && linkTag l == "sha256" =+			maybe (Left "sha256 hash parse failed")+				(Right . BlobLink)+				(parseSha256 $ encodeUtf8 $ linkTo l)+		| otherwise = Left "wrong sigil for blob link"+	toLink fl = Link '&' linkto "sha256"+	  where+		linkto = decodeUtf8 $ formatHash $ unBlobLink fl++instance Aeson.ToJSON BlobLink where+	toJSON = toJSONLink++instance Aeson.FromJSON BlobLink where+	parseJSON = fromJSONLink
src/Ssb/Types/Message.hs view
@@ -2,9 +2,10 @@  module Ssb.Types.Message ( 	Signature,-	HashType, 	Message(..),+	messageLink, 	AnyContent(..),+	parseMessage, 	narrowParse, 	PrivateContent(..), 	Post(..),@@ -17,25 +18,24 @@ 	parseMessageType ) where -import Ssb.Types.Key import Ssb.Types.Link+import Ssb.Types.Hash  import GHC.Generics import Data.Aeson import Data.Aeson.Types import Data.Int (Int64)-import qualified Data.Text as T import Data.Maybe import Data.List import Data.Char import Control.Applicative+import qualified Data.Text as T+import qualified Data.ByteString.Lazy as L  import Prelude hiding (sequence)  type Signature = String -type HashType = String- data Message a = Message 	{ previous :: Maybe MessageLink 	, author :: FeedLink@@ -52,6 +52,14 @@ instance Functor Message where 	fmap f m = m { content = f (content m) } +messageLink :: Hashed (Message a) -> MessageLink+messageLink hm = MessageLink (hashOf hm)++-- | Parses a JSON formatted message, and accompanies it with the +-- hash that was originally used for the message.+parseMessage :: FromJSON a => L.ByteString -> Maybe (Hashed (Message a))+parseMessage b = calcHashed b hash decode+ -- | Parsing a Message AnyContent allows parsing the message envelope, -- regardless of the type of content in the message. data AnyContent = AnyContent { fromAnyContent :: Value }@@ -103,7 +111,7 @@  -- | A generic reference to other feeds, entities, or blobs data Mention = Mention-	{ mentionLink :: GenericLink+	{ mentionLink :: Link 	} deriving (Show, Eq, Generic)  instance FromJSON Mention where@@ -113,7 +121,7 @@ -- They can be used to set a name or picture for users, files, or messages. -- However, they're most commonly published about users. data About = About-	{ about :: GenericLink+	{ about :: Link 	, name :: Maybe T.Text 	, image :: Maybe AboutImage 	} deriving (Show, Eq, Generic)@@ -149,7 +157,7 @@ -- | Vote-messages signal approval about someone or something. -- Votes can be on users, messages, or blobs. data Vote = Vote-	{ voteLink :: GenericLink+	{ voteLink :: Link 	, voteValue :: Int 	, voteExpression :: Maybe T.Text 	} deriving (Show, Eq, Generic)@@ -179,7 +187,7 @@ data Pub = Pub 	{ pubHost :: T.Text 	, pubPort :: Int-	, pubKey :: PublicKey+	, pubKey :: FeedLink 	} deriving (Show, Eq, Generic)  instance FromJSON Pub where
+ test/HashSpec.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE OverloadedStrings #-}++module HashSpec where++import Ssb.Types.Hash+import Test.Hspec+import Data.ByteString++base64sha256 :: ByteString+base64sha256 = "MPB9vxHO0pvi2ve2wh6Do05ZrV7P6ZjUQ+IEYnzLfTs="++spec :: Spec+spec = describe "sha256 hash" $ do+	it "can be deserialized and serialized from base64 bytestrings" $ do+		let (Just hash) = parseSha256 base64sha256+		(formatHash hash) `shouldBe` base64sha256
test/KeySpec.hs view
@@ -7,10 +7,10 @@ import Data.ByteString  base64PublicKey :: ByteString-base64PublicKey = "@opBinLmYiID9SMEoZJPH60LmduSYAR7J00P7/Gj9vHw=.ed25519"+base64PublicKey = "opBinLmYiID9SMEoZJPH60LmduSYAR7J00P7/Gj9vHw="  spec :: Spec spec = describe "public key" $ do 	it "can be deserialized and serialized from base64 bytestrings" $ do-		let (Just publicKey) = parsePublicKey base64PublicKey+		let (Just publicKey) = parseEd25519PublicKey base64PublicKey 		(formatPublicKey publicKey) `shouldBe` base64PublicKey
+ test/LinkSpec.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings #-}++module LinkSpec where++import Ssb.Types.Link+import Ssb.Types.Key+import Test.Hspec+import Data.Text+import Data.Maybe++feedLink :: Text+feedLink = "@LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=.ed25519"++spec :: Spec+spec = do+	describe "Link" $ do+		it "parses" $ do+			let (Right l) = parseLink feedLink+			linkSigil l `shouldBe` '@'+			linkTo l `shouldBe` "LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs="+			linkTag l `shouldBe` "ed25519"+			formatLink l `shouldBe` feedLink+	describe "FeedLink" $ do+		it "parses" $ do+			let (Right l) = fromLink =<< parseLink feedLink+			unFeedLink l `shouldBe` fromJust (parseEd25519PublicKey "LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=")
test/MessageSpec.hs view
@@ -5,10 +5,11 @@  import Test.Hspec import Ssb.Types.Message-import Ssb.Types.Key import Ssb.Types.Link+import Ssb.Types.Key+import Ssb.Types.Hash import Data.Aeson-import Data.ByteString.Lazy+import Data.ByteString.Lazy (ByteString) import Data.Maybe  import Prelude hiding (sequence)@@ -50,7 +51,7 @@ messageSpec = describe "message" $ do 	it "extracts message content" $ do 		let (Just message) = (decode originalMessage) :: Maybe (Message AnyContent)-		previous message `shouldBe` Just "%3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y=.sha256"+		previous message `shouldBe` MessageLink <$> parseSha256 "3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y=" 		author message `shouldBe` fromJust (FeedLink <$> parseEd25519PublicKey "EMovhfIrFk4NihAKnRNhrfRaqIhBv1Wj8pTxJNgvCCY=") 		sequence message `shouldBe` 184 		timestamp message `shouldBe` 1449954503740@@ -61,16 +62,16 @@ postSpec = describe "post" $ do 	it "extracts message post" $ do 		let (Just message) = (decode originalMessage) :: Maybe (Message Post)-		content message `shouldBe` Post-			{ text = "@johnny use https://github.com/ssbc/ssb-msgs and https://github.com/ssbc/ssb-ref is probably useful too."-			, channel = Nothing-			, root = Just "%rf1JvoFg1pHE6TkuuMxsjBNevFck7LQvXGhkLMNlaYs=.sha256"-			, branch = Just "%G37a4PUTbysiETQazyYATwLkfn/ZzigmIheAx905MLU=.sha256"-			, recps = Nothing-			, mentions = Just-				[ Mention { mentionLink = "@dnr1swLSAgf36g+FzGjNLgmytj2IIyDaYeKZ7F5GdzY=.ed25519" }-				]-			}+		text (content message) `shouldBe`+			"@johnny use https://github.com/ssbc/ssb-msgs and https://github.com/ssbc/ssb-ref is probably useful too."+		channel (content message) `shouldBe` Nothing+		root (content message) `shouldBe`+			MessageLink <$> parseSha256 "rf1JvoFg1pHE6TkuuMxsjBNevFck7LQvXGhkLMNlaYs="+		branch (content message) `shouldBe`+			MessageLink <$> parseSha256 "G37a4PUTbysiETQazyYATwLkfn/ZzigmIheAx905MLU="+		recps (content message) `shouldBe` Nothing+		map (linkTo . mentionLink) <$> (mentions (content message)) `shouldBe`+			Just [ "dnr1swLSAgf36g+FzGjNLgmytj2IIyDaYeKZ7F5GdzY=" ]  	it "does not parse content that is not a post" $ do 		(decode originalAbout :: Maybe (Message Post)) `shouldBe` Nothing@@ -101,10 +102,10 @@ aboutSpec = describe "about" $ do 	it "extracts message about" $ do 		let Just message = (decode originalAbout) :: Maybe (Message About)-		about (content message) `shouldBe` "@4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=.ed25519"+		linkTo (about (content message)) `shouldBe` "4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=" 		name (content message) `shouldBe` Nothing 		fmap aboutImageLink (image (content message)) `shouldBe`-			Just "&Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM=.sha256"+			BlobLink <$> parseSha256 "Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM=" 		fmap aboutImageSize (image (content message)) `shouldBe` 			Just (Just 502712) 		fmap aboutImageType (image (content message)) `shouldBe`@@ -162,11 +163,9 @@ voteSpec = describe "vote" $ do 	it "extracts message vote" $ do 		let Just message = (decode originalVote) :: Maybe (Message Vote)-		content message `shouldBe` Vote-			{ voteLink = "%lu9JsdUl4astKykyaVFdkLb7//tS0K08BMs6dT+fErA=.sha256"-			, voteValue = 1-			, voteExpression = Nothing-			}+		voteValue (content message) `shouldBe` 1+		voteExpression (content message) `shouldBe` Nothing+		linkTo (voteLink (content message)) `shouldBe` "lu9JsdUl4astKykyaVFdkLb7//tS0K08BMs6dT+fErA="  -- Not a real private message content, because the test couldn't decrypt it anyway. originalPrivateContent :: ByteString@@ -213,5 +212,5 @@ 		let Just message = (decode originalPub) :: Maybe (Message Pub) 		pubHost (content message) `shouldBe` "ssb.hypersignal.xyz" 		pubPort (content message) `shouldBe` 8008-		Just (pubKey (content message)) `shouldBe`+		Just (unFeedLink (pubKey (content message))) `shouldBe` 			parseEd25519PublicKey "XRg7pXoQqsWDDk4dmgvSWHUqzwS6BmqMo4IdbMKPjWA="