packages feed

scuttlebutt-types (empty) → 0.1.0

raw patch · 12 files changed

+647/−0 lines, 12 filesdep +aesondep +basedep +base64-bytestringsetup-changed

Dependencies added: aeson, base, base64-bytestring, bytestring, bytestring-conversion, ed25519, hspec, scuttlebutt-types, text

Files

+ CHANGELOG view
@@ -0,0 +1,5 @@+scuttlebutt-types (0.1.0) unstable; urgency=low++  * First release, split off from haskell-scuttlebutt.++ -- Joey Hess <id@joeyh.name>  Thu, 05 Oct 2017 12:47:48 -0400
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2017, bkil.hu, Peter Ferenc Hajdu, Joey Hess+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,23 @@+Secure Scuttlebutt is a database of unforgeable append-only feeds,+optimized for efficient replication for peer to peer protocols.+<https://www.scuttlebutt.nz/>++This library contains Haskell data types for common Secure Scuttlebutt+messages, including JSON serialization.++Git repositories:++* https://git.joeyh.name/git/haskell-scuttlebutt-types.git+* git-ssb repository:+  ssb://%h2FnIacUWXelN8YGY0NcShMjQCqkcDF8XVpW64LgTHE=.sha256++Contributions welcome! Please use git-ssb or email for making pull requests+and issue tracking. Also you can find fellow haskellers on the #haskell+channel on SSB, and discuss this library there.++Other Haskell Scuttlebutt resources:++* The haskell-scuttlebutt git repository is built on top of+  scuttlebutt-types, and has dreams of one day growing up to be a+  real scuttlebutt client. It git-ssb repository is here:+  ssb://%Nk2lTG8YHweKZEAfDrmzyjrkHPX1C9jHmYqc6csTCdU=.sha256
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ scuttlebutt-types.cabal view
@@ -0,0 +1,56 @@+Name: scuttlebutt-types+Version: 0.1.0+License: BSD3+License-File: LICENSE+Author: bkil.hu, Peter Ferenc Hajdu, Joey Hess+Maintainer: Joey Hess <id@joeyh.name>+Copyright: 2017 bkil.hu, Peter Ferenc Hajdu, Joey Hess+Category: Network+Build-Type: Simple+Extra-Source-Files: README.md CHANGELOG+Cabal-Version: >=1.10+Synopsis: generic types for Secure Scuttlebutt+Description:+ Secure Scuttlebutt is a database of unforgeable append-only feeds,+ optimized for efficient replication for peer to peer protocols.+ .+ This library contains data types for common Scuttlebutt messages,+ including JSON serialization.++Library+  Hs-Source-Dirs: src+  GHC-Options: -Wall -fno-warn-tabs+  Exposed-Modules:+    Ssb.Types+    Ssb.Types.Message+    Ssb.Types.Key+    Ssb.Types.Link+  Build-Depends:+    base (>= 4.7 && < 5),+    bytestring,+    bytestring-conversion,+    base64-bytestring,+    aeson,+    ed25519,+    text+  Default-Language: Haskell2010++Test-Suite scuttlebutt-types-test+  Type: exitcode-stdio-1.0+  Hs-Source-Dirs: test+  Main-Is: Spec.hs+  Build-Depends:+    base,+    scuttlebutt-types,+    hspec,+    bytestring,+    aeson+  Other-Modules:+    MessageSpec+    KeySpec+  Ghc-Options: -Wall -fno-warn-tabs -threaded -rtsopts -with-rtsopts=-N+  Default-Language: Haskell2010++source-repository head+  type: git+  location: https://git.joeyh.name/git/haskell-scuttlebutt-types.git
+ src/Ssb/Types.hs view
@@ -0,0 +1,7 @@+module Ssb.Types (+	module Ssb.Types.Message,+	module Ssb.Types.Key+) where++import Ssb.Types.Message+import Ssb.Types.Key
+ src/Ssb/Types/Key.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}++module Ssb.Types.Key (+	PublicKey(..),+	parsePublicKey,+	parseEd25519PublicKey,+	formatPublicKey,+) where++import Data.ByteString+import Data.Text.Encoding (encodeUtf8, 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++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+  where+	decodedPublicKey = B64.decode b+	maybeEd25519PublicKey = either+		(const Nothing)+		(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.+formatPublicKey :: PublicKey -> ByteString+formatPublicKey (Ed25519PublicKey { ed25519Key = k }) =+	"@" <> B64.encode (Ed.unPublicKey k) <> ".ed25519"
+ src/Ssb/Types/Link.hs view
@@ -0,0 +1,26 @@+-- | TODO newtypes for everything++module Ssb.Types.Link where++import Ssb.Types.Key++import Data.Aeson++-- | A link to a message.+type MessageLink = String++-- | A link to a feed.+newtype FeedLink = FeedLink { unFeedLink :: PublicKey }+	deriving (Show, Eq, Ord)++instance FromJSON FeedLink where+	parseJSON d = FeedLink <$> parseJSON d++instance ToJSON FeedLink where+	toJSON = toJSON . unFeedLink++-- | A link to a blob.+type BlobLink = String++-- | A link to a message, feed, or blob.+type GenericLink = String
+ src/Ssb/Types/Message.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE DeriveGeneric, OverloadedStrings, FlexibleContexts #-}++module Ssb.Types.Message (+	Signature,+	HashType,+	Message(..),+	AnyContent(..),+	narrowParse,+	PrivateContent(..),+	Post(..),+	Mention(..),+	About(..),+	AboutImage(..),+	Contact(..),+	Vote(..),+	Pub(..),+	parseMessageType+) where++import Ssb.Types.Key+import Ssb.Types.Link++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 Prelude hiding (sequence)++type Signature = String++type HashType = String++data Message a = Message+	{ previous :: Maybe MessageLink+	, author :: FeedLink+	, sequence :: Int64+	, timestamp :: Float+	, hash :: HashType+	, content :: a+	, signature :: Signature+	} deriving (Show, Eq, Generic)++instance FromJSON a => FromJSON (Message a)+instance ToJSON a => ToJSON (Message a)++instance Functor Message where+	fmap f m = m { content = f (content m) }++-- | Parsing a Message AnyContent allows parsing the message envelope,+-- regardless of the type of content in the message.+data AnyContent = AnyContent { fromAnyContent :: Value }+	deriving (Show, Eq)++instance FromJSON AnyContent where+	parseJSON = pure . AnyContent++-- | For best efficiency when the type of a message is not known,+-- first parse to a Message AnyContent, and then use this function+-- with `parseMaybe` or `parseEither` to try to further parse that+-- to different message types.+--+-- For example:+--+-- > Just somemsg = decode b :: Maybe Message AnyContent+-- > case parseMaybe narrowParse somemsg :: Maybe (Message Post) of+-- >   Just postmsg -> ...+-- >   Nothing -> case parseMaybe narrowParse somemsg :: Maybe (Message PrivateContent) of+-- >     Just privmsg -> ...+-- >     Nothing -> ...+narrowParse :: FromJSON a => Message AnyContent -> Parser (Message a)+narrowParse m = do+	c <- parseJSON (fromAnyContent (content m))+	return $ m { content = c }++-- | A message with encrypted content.+data PrivateContent = PrivateContent T.Text+	deriving (Show, Eq)++-- Any message that has a content that is a string, rather than a JSON+-- object, is an encrypted message.+instance FromJSON PrivateContent where+	parseJSON = withText "PrivateContent" (pure . PrivateContent)++-- | A post is a text-based message, for a public or private audience.+-- It can be a reply to other posts.+data Post = Post+	{ text :: T.Text+	, channel :: Maybe T.Text+	, root :: Maybe MessageLink+	, branch :: Maybe MessageLink+	, recps :: Maybe [FeedLink]+	, mentions :: Maybe [Mention]+	} deriving (Show, Eq, Generic)++instance FromJSON Post where+	parseJSON = parseMessageType "post" (genericParseJSON defaultOptions)++-- | A generic reference to other feeds, entities, or blobs+data Mention = Mention+	{ mentionLink :: GenericLink+	} deriving (Show, Eq, Generic)++instance FromJSON Mention where+	parseJSON = parseStrippingPrefix "mention"++-- | About-messages set attributes about someone or something.+-- 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+	, name :: Maybe T.Text+	, image :: Maybe AboutImage+	} deriving (Show, Eq, Generic)++instance FromJSON About where+	parseJSON = parseMessageType "about" (genericParseJSON defaultOptions)++data AboutImage = AboutImage+	{ aboutImageLink :: BlobLink+	, aboutImageSize :: Maybe Int+	, aboutImageType :: Maybe T.Text+	, aboutImageWidth :: Maybe Int+	, aboutImageHeight :: Maybe Int+	} deriving (Show, Eq, Generic)++instance FromJSON AboutImage where+	parseJSON = parseStrippingPrefix "aboutimage"++-- | Contact-messages determine who you are following or blocking.+data Contact = Contact+	{ contact :: FeedLink+	, following :: Bool+	, blocking :: Bool+	} deriving (Show, Eq, Generic)++instance FromJSON Contact where+	parseJSON = parseMessageType "contact" $+		withObject "Contect" $ \o -> Contact+			<$> o .: "contact"+			<*> o .:? "following" .!= False+			<*> o .:? "blocking" .!= False++-- | Vote-messages signal approval about someone or something.+-- Votes can be on users, messages, or blobs.+data Vote = Vote+	{ voteLink :: GenericLink+	, voteValue :: Int+	, voteExpression :: Maybe T.Text+	} deriving (Show, Eq, Generic)++instance FromJSON Vote where+	parseJSON = parseMessageType "vote" $ withObject "Vote" $ \o -> do+		-- For some reason the JSON wraps the vote in another object.+		v <- o .: "vote"+		Vote+			<$> v .: "link"+			<*> (v .: "value" <|> stringvalue v)+			<*> v .:? "expression"+	  where+		-- It's not uncommon for the value to be a string containing a+		-- number, although it's supposed to be a plain number.+		stringvalue v = do+			s <- v .: "value" :: Parser T.Text+			case s of+				"1" -> return 1+				"0" -> return 0+				"-1" -> return (-1)+				_ -> fail "unknown vote value"++-- Pub-messages announce the address, port, and public key of pubs.+-- They are automatically published by Scuttlebot after successfully+-- using an invite to a pub.+data Pub = Pub+	{ pubHost :: T.Text+	, pubPort :: Int+	, pubKey :: PublicKey+	} deriving (Show, Eq, Generic)++instance FromJSON Pub where+	parseJSON = parseMessageType "pub" $ withObject "Pub" $ \o -> do+		-- For some reason the JSON wraps the pub in another object.+		v <- o .: "address"+		parseStrippingPrefix "pub" v++-- | Parse the content of a message using the provided Parser,+-- which will typically be genericParseJSON defaultOptions.+--+-- The "type" field must contain the specified Text for the parse to succeed.+parseMessageType :: T.Text -> (Value -> Parser a) -> Value -> Parser a+parseMessageType ty parser v@(Object o) = do+	t <- o .: "type"+	if t == ty+		then parser v+		else fail $ "wrong message type " ++ T.unpack t ++ " (expected " ++ T.unpack ty ++ ")"+parseMessageType ty _ invalid = typeMismatch (T.unpack ty) invalid++-- | Parse, stripping a common prefix from the haskell record names.+parseStrippingPrefix :: (Generic a, GFromJSON Zero (Rep a)) => String -> Value -> Parser a+parseStrippingPrefix prefix = +	genericParseJSON $ defaultOptions { fieldLabelModifier = f }+  where+  	f s = fromMaybe s $ stripPrefix prefix $ map toLower s
+ test/KeySpec.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE OverloadedStrings #-}++module KeySpec where++import Ssb.Types.Key+import Test.Hspec+import Data.ByteString++base64PublicKey :: ByteString+base64PublicKey = "@opBinLmYiID9SMEoZJPH60LmduSYAR7J00P7/Gj9vHw=.ed25519"++spec :: Spec+spec = describe "public key" $ do+	it "can be deserialized and serialized from base64 bytestrings" $ do+		let (Just publicKey) = parsePublicKey base64PublicKey+		(formatPublicKey publicKey) `shouldBe` base64PublicKey
+ test/MessageSpec.hs view
@@ -0,0 +1,217 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module MessageSpec where++import Test.Hspec+import Ssb.Types.Message+import Ssb.Types.Key+import Ssb.Types.Link+import Data.Aeson+import Data.ByteString.Lazy+import Data.Maybe++import Prelude hiding (sequence)++spec :: Spec+spec = do+	messageSpec+	postSpec+	aboutSpec+	contactSpec+	voteSpec+	privateContentSpec+	pubSpec++originalMessage :: ByteString+originalMessage = "\+\{\+\  \"previous\": \"%3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y=.sha256\",\+\  \"author\": \"@EMovhfIrFk4NihAKnRNhrfRaqIhBv1Wj8pTxJNgvCCY=.ed25519\",\+\  \"sequence\": 184,\+\  \"timestamp\": 1449954503740,\+\  \"hash\": \"sha256\",\+\  \"content\": {\+\    \"type\": \"post\",\+\    \"text\": \"@johnny use https://github.com/ssbc/ssb-msgs and https://github.com/ssbc/ssb-ref is probably useful too.\",\+\    \"root\": \"%rf1JvoFg1pHE6TkuuMxsjBNevFck7LQvXGhkLMNlaYs=.sha256\",\+\    \"branch\": \"%G37a4PUTbysiETQazyYATwLkfn/ZzigmIheAx905MLU=.sha256\",\+\    \"mentions\": [\+\      {\+\        \"link\": \"@dnr1swLSAgf36g+FzGjNLgmytj2IIyDaYeKZ7F5GdzY=.ed25519\",\+\        \"name\": \"johnny\"\+\      }\+\    ]\+\  },\+\  \"signature\": \"28GiO52GFjJnrwpKo359FamEes7JB9gTiiZaidKLL1C1NRueqGq2IAYQ1V+T2AnBgUJLRZUIyNtLTlBcx4RGAA==.sig.ed25519\"\+\}"++messageSpec :: Spec+messageSpec = describe "message" $ do+	it "extracts message content" $ do+		let (Just message) = (decode originalMessage) :: Maybe (Message AnyContent)+		previous message `shouldBe` Just "%3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y=.sha256"+		author message `shouldBe` fromJust (FeedLink <$> parseEd25519PublicKey "EMovhfIrFk4NihAKnRNhrfRaqIhBv1Wj8pTxJNgvCCY=")+		sequence message `shouldBe` 184+		timestamp message `shouldBe` 1449954503740+		hash message `shouldBe` "sha256"+		signature message `shouldBe` "28GiO52GFjJnrwpKo359FamEes7JB9gTiiZaidKLL1C1NRueqGq2IAYQ1V+T2AnBgUJLRZUIyNtLTlBcx4RGAA==.sig.ed25519"++postSpec :: Spec+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" }+				]+			}++	it "does not parse content that is not a post" $ do+		(decode originalAbout :: Maybe (Message Post)) `shouldBe` Nothing++originalAbout :: ByteString+originalAbout = "\+\{\+\  \"previous\": \"%6+FS+LB8nDkgHZKanIB6kK5AYBK2SZYH55q5zv/5XIQ=.sha256\",\+\  \"author\": \"@4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=.ed25519\",\+\  \"sequence\": 5,\+\  \"timestamp\": 1464267463411,\+\  \"hash\": \"sha256\",\+\  \"content\": {\+\    \"type\": \"about\",\+\    \"about\": \"@4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=.ed25519\",\+\    \"image\": {\+\      \"link\": \"&Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM=.sha256\",\+\      \"size\": 502712,\+\      \"type\": \"image/png\",\+\      \"width\": 512,\+\      \"height\": 512\+\    }\+\  },\+\  \"signature\": \"mPC156lndHN2oES/UE0GPK/Z8QgFLG8B/bba2TycXzPZZ7IShQhmFiasE0r0ZBTfRmU9KyuVdfwVYogkY2IACA==.sig.ed25519\"\+\}"++aboutSpec :: Spec+aboutSpec = describe "about" $ do+	it "extracts message about" $ do+		let Just message = (decode originalAbout) :: Maybe (Message About)+		about (content message) `shouldBe` "@4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=.ed25519"+		name (content message) `shouldBe` Nothing+		fmap aboutImageLink (image (content message)) `shouldBe`+			Just "&Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM=.sha256"+		fmap aboutImageSize (image (content message)) `shouldBe`+			Just (Just 502712)+		fmap aboutImageType (image (content message)) `shouldBe`+			Just (Just "image/png")+		fmap aboutImageWidth (image (content message)) `shouldBe`+			Just (Just 512)+		fmap aboutImageHeight (image (content message)) `shouldBe`+			Just (Just 512)++originalContact :: ByteString+originalContact = "\+\{\+\  \"previous\": \"%kLJuVKntE9LsPeTSyiVSNnb2vkiGXzBUMeDPoGr6BBw=.sha256\",\+\  \"author\": \"@ye+QM09iPcDJD6YvQYjoQc7sLF/IFhmNbEqgdzQo3lQ=.ed25519\",\+\  \"sequence\": 5442,\+\  \"timestamp\": 1488618790507,\+\  \"hash\": \"sha256\",\+\  \"content\": {\+\    \"type\": \"contact\",\+\    \"contact\": \"@o91tsjBgL0l0zs475aRWYSAXFxvirZS7VrXmirY/msI=.ed25519\",\+\    \"following\": true\+\  },\+\  \"signature\": \"zknuPbUwMeBaDZzZ9iBETOPbZ7SYqetd3RVkq2mYSyr4QI/7uwho3GnerIF3fLcvW3MgRM5SvFvSTnpxxQFoCg==.sig.ed25519\"\+\}"++contactSpec :: Spec+contactSpec = describe "contact" $ do+	it "extracts message contact" $ do+		let Just message = (decode originalContact) :: Maybe (Message Contact)+		content message `shouldBe` Contact+			{ contact = fromJust $ FeedLink <$> parseEd25519PublicKey "o91tsjBgL0l0zs475aRWYSAXFxvirZS7VrXmirY/msI="+			, following = True+			, blocking = False+			}++originalVote :: ByteString+originalVote = "\+\{\+\ \"previous\": \"%jiyo59Or2Rrm+05QJIZ/fLfCeBhiaJ+tjJgW/USgtYs=.sha256\",\+\  \"author\": \"@/02iw6SFEPIHl8nMkYSwcCgRWxiG6VP547Wcp1NW8Bo=.ed25519\",\+\  \"sequence\": 3304,\+\  \"timestamp\": 1459235596255,\+\  \"hash\": \"sha256\",\+\  \"content\": {\+\    \"type\": \"vote\",\+\    \"vote\": {\+\      \"link\": \"%lu9JsdUl4astKykyaVFdkLb7//tS0K08BMs6dT+fErA=.sha256\",\+\      \"value\": 1\+\    }\+\  },\+\  \"signature\": \"iw/wREQJ4UEecnfuFNIiZt9kMZXkQc8SP2jmhVbUvPj9Eo50D2yJKfza+Qf/tYwOuFaE9et4pbucPD/4OMlbAg==.sig.ed25519\"\+\}"++voteSpec :: Spec+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+			}++-- Not a real private message content, because the test couldn't decrypt it anyway.+originalPrivateContent :: ByteString+originalPrivateContent = "\+\{\+\  \"previous\": \"%RFY6DRn3syOm7GodwpNTePx0I4ZWyZMVsmZ4V1QqiDQ=.sha256\",\+\  \"author\": \"@9nTgtYmvW4HID6ayt6Icwc8WZxdifx5SlSKKIX/X/1g=.ed25519\",\+\  \"sequence\": 245,\+\  \"timestamp\": 1481349557377,\+\  \"hash\": \"sha256\",\+\  \"content\": \"dummy\",\+\  \"signature\": \"2urg9PG4+esti1DIf4DnFfsUXXnOzwXHv+QnflKQQ0145pNw+SIXeYkhNhzqdFm2fz7wzWb7YVlFMPIL11EaBQ==.sig.ed25519\"\+\}"++privateContentSpec :: Spec+privateContentSpec = describe "private" $ do+	it "extracts message private" $ do+		let Just message = (decode originalPrivateContent) :: Maybe (Message PrivateContent)+		author message `shouldBe` fromJust (FeedLink <$> parseEd25519PublicKey "9nTgtYmvW4HID6ayt6Icwc8WZxdifx5SlSKKIX/X/1g=")+		content message `shouldBe` PrivateContent "dummy"++originalPub :: ByteString+originalPub = "\+\{\+\  \"previous\": \"%qYdhzyTbQwrdTOpKVvFS0nFCOvK5SekDhxLjHs6i1jo=.sha256\",\+\  \"author\": \"@26RTWG0+zR1DLO43p4VjB/UzTNKgQ+TNvLey37E/YdI=.ed25519\",\+\  \"sequence\": 3,\+\  \"timestamp\": 1497928539321,\+\  \"hash\": \"sha256\",\+\  \"content\": {\+\    \"type\": \"pub\",\+\    \"address\": {\+\      \"host\": \"ssb.hypersignal.xyz\",\+\      \"port\": 8008,\+\      \"key\": \"@XRg7pXoQqsWDDk4dmgvSWHUqzwS6BmqMo4IdbMKPjWA=.ed25519\"\+\    }\+\  },\+\  \"signature\": \"nWBpjqbnw4sGkaD+0OpxzmTo5VeGOnRcj2hxWjATlO9e7F9eK2yJACOaa5hzGT1E967uFhwmvwUA6FMq3Z0MBQ==.sig.ed25519\"\+\}"++pubSpec :: Spec+pubSpec = describe "pub" $ do+	it "extracts message pub" $ do+		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`+			parseEd25519PublicKey "XRg7pXoQqsWDDk4dmgvSWHUqzwS6BmqMo4IdbMKPjWA="
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}