matrix-client-0.1.6.1: src/Network/Matrix/Identity.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{- | This module contains the Identity service API
https://matrix.org/docs/spec/identity_service/r0.3.0.html
-}
module Network.Matrix.Identity (
-- * Client
IdentitySession,
MatrixToken (..),
getTokenFromEnv,
createIdentitySession,
-- * API
MatrixIO,
MatrixError (..),
retry,
retryWithLog,
-- * User data
UserID (..),
getIdentityTokenOwner,
-- * Association lookup
HashDetails (..),
hashDetails,
Identity (..),
identityLookup,
HashedAddress,
IdentityLookupRequest,
IdentityLookupResponse,
identitiesLookup,
mkIdentityLookupRequest,
toHashedAddress,
lookupIdentity,
)
where
import Control.Monad (mzero)
import Data.Aeson (FromJSON (..), Value (Object, String), encode, object, (.:), (.=))
import Data.Base64.Types (extractBase64)
import Data.ByteString.Lazy (fromStrict)
import Data.ByteString.Lazy.Base64.URL (encodeBase64Unpadded)
import Data.Digest.Pure.SHA (bytestringDigest, sha256)
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as KeyMap
#endif
import qualified Data.HashMap.Strict as HM
import Data.List (lookup)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.Lazy (toStrict)
import qualified Network.HTTP.Client as HTTP
import Network.Matrix.Internal
#if MIN_VERSION_aeson(2,0,0)
toKVList :: KeyMap.KeyMap v -> [(Text, v)]
toKVList = HM.toList . KeyMap.toHashMapText
#else
toKVList :: HM.HashMap Text v -> [(Text, v)]
toKVList = HM.toList
#endif
{- $setup
>>> import Data.Aeson (decode)
-}
-- | The session record, use 'createSession' to create it.
data IdentitySession = IdentitySession
{ baseUrl :: Text
, token :: MatrixToken
, manager :: HTTP.Manager
}
-- | 'createSession' creates the session record.
createIdentitySession ::
-- | The matrix identity base url, e.g. "https://matrix.org"
Text ->
-- | The user identity token
MatrixToken ->
IO IdentitySession
createIdentitySession baseUrl' token' = IdentitySession baseUrl' token' <$> mkManager
mkRequest :: IdentitySession -> Bool -> Text -> IO HTTP.Request
mkRequest IdentitySession{..} = mkRequest' baseUrl token
doRequest :: (FromJSON a) => IdentitySession -> HTTP.Request -> MatrixIO a
doRequest IdentitySession{..} = doRequest' manager
-- | 'getIdentityTokenOwner' gets information about the owner of a given access token.
getIdentityTokenOwner :: IdentitySession -> MatrixIO UserID
getIdentityTokenOwner session =
doRequest session =<< mkRequest session True "/_matrix/identity/v2/account"
data HashDetails = HashDetails
{ hdAlgorithms :: NonEmpty Text
, hdPepper :: Text
}
deriving (Show, Eq)
instance FromJSON HashDetails where
parseJSON (Object v) = HashDetails <$> v .: "algorithms" <*> v .: "lookup_pepper"
parseJSON _ = mzero
hashDetails :: IdentitySession -> MatrixIO HashDetails
hashDetails session =
doRequest session =<< mkRequest session True "/_matrix/identity/v2/hash_details"
-- | Use 'identityLookup' to lookup a single identity, otherwise uses the full 'identitiesLookup'.
identityLookup :: IdentitySession -> HashDetails -> Identity -> MatrixIO (Maybe UserID)
identityLookup session hd ident = do
fmap toUserIDM <$> identitiesLookup session ilr
where
toUserIDM = lookupIdentity address
address = toHashedAddress hd ident
ilr = mkIdentityLookupRequest hd [address]
data IdentityLookupRequest = IdentityLookupRequest
{ ilrHash :: Text
, ilrPepper :: Text
, ilrAddresses :: [HashedAddress]
}
deriving (Show, Eq)
newtype HashedAddress = HashedAddress Text deriving (Show, Eq)
{- | A newtype wrapper to decoded nested list
>>> decode "{\"mappings\": {\"hash\": \"user\"}}" :: Maybe IdentityLookupResponse
Just (IdentityLookupResponse [(HashedAddress "hash",UserID "user")])
-}
newtype IdentityLookupResponse = IdentityLookupResponse [(HashedAddress, UserID)]
deriving (Show)
instance FromJSON IdentityLookupResponse where
parseJSON (Object v) = do
mappings <- v .: "mappings"
case mappings of
(Object kv) -> pure . IdentityLookupResponse $ mapMaybe toTuple (toKVList kv)
_ -> mzero
where
toTuple (k, String s) = Just (HashedAddress k, UserID s)
toTuple _ = Nothing
parseJSON _ = mzero
identitiesLookup :: IdentitySession -> IdentityLookupRequest -> MatrixIO IdentityLookupResponse
identitiesLookup session ilr = do
request <- mkRequest session True "/_matrix/identity/v2/lookup"
doRequest
session
( request
{ HTTP.method = "POST"
, HTTP.requestBody = HTTP.RequestBodyLBS body
}
)
where
getAddr (HashedAddress x) = x
body =
encode $
object
[ "addresses" .= map getAddr (ilrAddresses ilr)
, "algorithm" .= ilrHash ilr
, "pepper" .= ilrPepper ilr
]
{- | Hash encoding for lookup
>>> encodeSHA256 "alice@example.com email matrixrocks"
"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc"
-}
encodeSHA256 :: Text -> Text
encodeSHA256 = toStrict . extractBase64 . encodeBase64Unpadded . bytestringDigest . sha256 . fromStrict . encodeUtf8
data Identity = Email Text | Msisdn Text deriving (Show, Eq)
toHashedAddress :: HashDetails -> Identity -> HashedAddress
toHashedAddress hd ident = HashedAddress $ encodeSHA256 $ val <> " " <> hdPepper hd
where
val = case ident of
Email x -> x <> " email"
Msisdn x -> x <> " msisdn"
mkIdentityLookupRequest :: HashDetails -> [HashedAddress] -> IdentityLookupRequest
mkIdentityLookupRequest hd = IdentityLookupRequest hash (hdPepper hd)
where
hash =
if "sha256" `elem` hdAlgorithms hd
then "sha256"
else error "Only sha256 is supported"
lookupIdentity :: HashedAddress -> IdentityLookupResponse -> Maybe UserID
lookupIdentity x (IdentityLookupResponse xs) = Data.List.lookup x xs