aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradambrangenberg <adabran06@gmail.com>2025-12-24 03:40:10 +0100
committeradambrangenberg <adabran06@gmail.com>2025-12-24 03:40:10 +0100
commita0886694f73fc382d78da79ab8bfb27475757bab (patch)
tree652ba9b603a1acaf4dfca188f7bb2c29c6bccfd0
parent2b48a574e8b9fed03a5c1969af4bb1e338f1be26 (diff)
Implemented basic auth, refactor
-rw-r--r--Makefile3
-rw-r--r--README.md8
-rw-r--r--cli/Main.hs41
-rw-r--r--jamaa.cabal109
-rw-r--r--make_data.sh18
-rw-r--r--make_endpoint.sh14
-rw-r--r--package.yaml25
-rw-r--r--src/Cors.hs4
-rw-r--r--src/Data/ContactData.hs22
-rw-r--r--src/Data/StandardErrorResponseData.hs18
-rw-r--r--src/Data/User.hs33
-rw-r--r--src/Data/VersionsData.hs17
-rw-r--r--src/Data/WellKnownClientData.hs34
-rw-r--r--src/Data/WellKnownSupportData.hs21
-rw-r--r--src/Database.hs18
-rw-r--r--src/Endpoints/APILib.hs12
-rw-r--r--src/Endpoints/LoginEndpoint.hs64
-rw-r--r--src/Endpoints/ProfileEndpoint.hs44
-rw-r--r--src/Endpoints/ServerLib.hs14
-rw-r--r--src/Endpoints/VersionsEndpoint.hs20
-rw-r--r--src/Endpoints/WellKnownClientEndpoint.hs12
-rw-r--r--src/Endpoints/WellKnownEndpoint.hs37
-rw-r--r--src/Endpoints/WellKnownSupportEndpoint.hs18
-rw-r--r--src/Lib_old.hs325
-rw-r--r--src/Model/Authentication.hs12
-rw-r--r--src/Model/Login.hs58
-rw-r--r--src/Model/MatrixErrorResponse.hs20
-rw-r--r--src/Model/Profile.hs26
-rw-r--r--src/Model/Versions.hs15
-rw-r--r--src/Model/WellKnown.hs51
-rw-r--r--src/PostLoginsResponseLib.hs10
-rw-r--r--src/QueryUserLib.hs11
-rw-r--r--src/RenameUtils.hs19
-rw-r--r--src/Util.hs18
34 files changed, 508 insertions, 663 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..70a8c6e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+run:
+ stack build
+ stack exec jamaa-exe \ No newline at end of file
diff --git a/README.md b/README.md
index a179288..86f80c6 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,14 @@
Started as a MuniHac Weekendproject (Graphlogic in Haskell maybe fun?).
Continuing it now as entertainment from boredom :)
+## Usage
+
+Add user to DB:
+`stack exec add-user -- <username> <password> [displayname]`
+
+Migrate DB:
+`stack exec add-user -- migrate`
+
## License
Copyright (c) 2025 Adam Brangenberg
diff --git a/cli/Main.hs b/cli/Main.hs
new file mode 100644
index 0000000..40c3da2
--- /dev/null
+++ b/cli/Main.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main(main) where
+
+import System.Environment (getArgs)
+import Database (runDb, runMigrations)
+import Data.User
+import Database.Persist
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Crypto.BCrypt (hashPasswordUsingPolicy, fastBcryptHashingPolicy)
+import qualified Data.ByteString.Base16 as Base16
+
+----------------------------------------------------------------------------------------------------
+main :: IO ()
+main = do
+ args <- getArgs
+
+ case args of
+ ["migrate"] -> do
+ _ <- runMigrations
+ putStrLn "Migrated"
+ [name, pass] -> do
+ hashed_password <- hashPassword (T.pack pass)
+
+ _ <- runDb $ insert $ User (T.pack name) hashed_password Nothing Nothing
+ putStrLn $ "User " ++ name ++ " created"
+ [name, pass, display_name] -> do
+ hashed_password <- hashPassword (T.pack pass)
+
+ _ <- runDb $ insert $ User (T.pack name) hashed_password (Just $ T.pack display_name) Nothing
+ putStrLn $ "User " ++ name ++ " created with display name '" ++ display_name ++ "'"
+ _ -> putStrLn "Usage: add-user <username> <password> [display_name]"
+ where
+ hashPassword :: T.Text -> IO T.Text
+ hashPassword pass = do
+ maybe_hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy (T.encodeUtf8 pass)
+
+ case maybe_hashed of
+ Just hashed -> return $ T.decodeUtf8 $ Base16.encode hashed
+ Nothing -> error "Password hashing failed"
diff --git a/jamaa.cabal b/jamaa.cabal
deleted file mode 100644
index ea361bc..0000000
--- a/jamaa.cabal
+++ /dev/null
@@ -1,109 +0,0 @@
-cabal-version: 2.2
-
--- This file has been generated from package.yaml by hpack version 0.37.0.
---
--- see: https://github.com/sol/hpack
-
-name: jamaa
-version: 0.1.0.0
-description: Please see the README on GitHub at <https://github.com/adambrangenberg/jamaa#readme>
-homepage: https://github.com/adambrangenberg/jamaa#readme
-bug-reports: https://github.com/adambrangenberg/jamaa/issues
-author: Adam Brangenberg
-maintainer: adam@adamwv.de
-copyright: Adam Brangenberg
-license: EUPL-1.2
-license-file: LICENSE
-build-type: Simple
-extra-source-files:
- README.md
- CHANGELOG.md
-
-source-repository head
- type: git
- location: https://github.com/adambrangenberg/jamaa
-
-library
- exposed-modules:
- Cors
- Data.ContactData
- Data.StandardErrorResponseData
- Data.VersionsData
- Data.WellKnownClientData
- Data.WellKnownSupportData
- Endpoints.APILib
- Endpoints.ServerLib
- Endpoints.VersionsEndpoint
- Endpoints.WellKnownClientEndpoint
- Endpoints.WellKnownSupportEndpoint
- Lib
- Lib_old
- PostLoginsResponseLib
- QueryUserLib
- RenameUtils
- other-modules:
- Paths_jamaa
- autogen-modules:
- Paths_jamaa
- hs-source-dirs:
- src
- ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
- build-depends:
- MissingH
- , aeson
- , base >=4.7 && <5
- , bytestring
- , servant-server
- , wai
- , wai-cors
- , wai-extra
- , warp
- default-language: Haskell2010
-
-executable jamaa-exe
- main-is: Main.hs
- other-modules:
- Paths_jamaa
- autogen-modules:
- Paths_jamaa
- hs-source-dirs:
- app
- ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
- build-depends:
- MissingH
- , aeson
- , base
- , bytestring
- , jamaa
- , servant-server
- , wai
- , wai-cors
- , wai-extra
- , warp
- default-language: Haskell2010
-
-test-suite jamaa-test
- type: exitcode-stdio-1.0
- main-is: Spec.hs
- other-modules:
- Paths_jamaa
- autogen-modules:
- Paths_jamaa
- hs-source-dirs:
- test
- ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
- build-depends:
- MissingH
- , aeson
- , base
- , bytestring
- , hspec
- , hspec-wai
- , hspec-wai-json
- , jamaa
- , servant-server
- , wai
- , wai-cors
- , wai-extra
- , warp
- default-language: Haskell2010
diff --git a/make_data.sh b/make_data.sh
deleted file mode 100644
index d35302c..0000000
--- a/make_data.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-echo "{-# LANGUAGE DeriveGeneric #-}
-
-module Data.$1Data ($1, make$1) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-
-data $1 = $1
- {
- } deriving (Eq, Show, Generic)
-
-instance FromJSON $1 where
-instance ToJSON $1 where
-
-make$1 :: -> $1
-make$1 = $1" > ./src/Data/$1Data.hs
-echo "Created $1Data.hs"
diff --git a/make_endpoint.sh b/make_endpoint.sh
deleted file mode 100644
index 15dbc17..0000000
--- a/make_endpoint.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-touch ./src/Endpoints/$1Endpoint.hs
-echo "{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeOperators #-}
-
-module Endpoints.$1Endpoint ($1API, handle$1) where
-
-import Servant
-import Data.$1Data
-
-type $1API = \"\" :> ... '[JSON] $1
-
-handle$1 :: Handler $1
-handle$1 =" > ./src/Endpoints/$1Endpoint.hs
-echo "Created $1Endpoint.hs"
diff --git a/package.yaml b/package.yaml
index 2f352e7..576f609 100644
--- a/package.yaml
+++ b/package.yaml
@@ -15,7 +15,7 @@ extra-source-files:
# category: Web
# To avoid duplicated efforts in documentation and dealing with the
-# complications of embedding Haddock markup inside cabal files, it is
+# complications of embedding Haddock markup inside cabal files it is
# common to point users to the README.md file.
description: Please see the README on GitHub at <https://github.com/adambrangenberg/jamaa#readme>
@@ -29,6 +29,15 @@ dependencies:
- MissingH
- bytestring
- wai-cors
+ - persistent
+ - persistent-sqlite
+ - persistent-template
+ - monad-logger
+ - resource-pool
+ - text
+ - mtl
+ - bcrypt
+ - base16-bytestring
ghc-options:
- -Wall
@@ -56,6 +65,20 @@ executables:
- base
- jamaa
+ add-user:
+ main: Main.hs
+ source-dirs: cli
+ ghc-options:
+ - -threaded
+ - -rtsopts
+ - -with-rtsopts=-N
+ dependencies:
+ - jamaa
+ - persistent
+ - text
+ - bcrypt
+ - base16-bytestring
+
tests:
jamaa-test:
main: Spec.hs
diff --git a/src/Cors.hs b/src/Cors.hs
index ccfcec5..906c7cc 100644
--- a/src/Cors.hs
+++ b/src/Cors.hs
@@ -8,16 +8,16 @@ import Network.Wai.Middleware.Cors
, CorsResourcePolicy(..)
)
import Network.Wai (Middleware)
-import Data.ByteString (ByteString)
matrixCorsPolicy :: CorsResourcePolicy
matrixCorsPolicy = simpleCorsResourcePolicy
- { corsOrigins = Just ([], True) -- Access-Control-Allow-Origin: *
+ { corsOrigins = Nothing -- Access-Control-Allow-Origin: *
, corsMethods = [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
, corsRequestHeaders =
[ "X-Requested-With"
, "Content-Type"
, "Authorization"
+ , "Date"
]
}
diff --git a/src/Data/ContactData.hs b/src/Data/ContactData.hs
deleted file mode 100644
index 397c465..0000000
--- a/src/Data/ContactData.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Data.ContactData (Contact, makeContact) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-
-type EMail = String
-type MXID = String
-
-data Contact = Contact
- { email_address :: EMail
- , matrix_id :: MXID
- , role :: String -- Enum? [m.role.admin, m.role.security]
- } deriving (Eq, Show, Generic)
-
-instance FromJSON Contact where
-instance ToJSON Contact where
-
-makeContact :: String -> String -> String -> Contact
-makeContact = Contact
diff --git a/src/Data/StandardErrorResponseData.hs b/src/Data/StandardErrorResponseData.hs
deleted file mode 100644
index f6fda87..0000000
--- a/src/Data/StandardErrorResponseData.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Data.StandardErrorResponseData (StandardErrorResponse, makeStandardErrorResponse) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-
-data StandardErrorResponse = StandardErrorResponse
- { errcode :: String -- TODO: Enum?
- , error :: String
- } deriving (Eq, Show, Generic)
-
-instance FromJSON StandardErrorResponse where
-instance ToJSON StandardErrorResponse where
-
-makeStandardErrorResponse :: String -> String -> StandardErrorResponse
-makeStandardErrorResponse = StandardErrorResponse
diff --git a/src/Data/User.hs b/src/Data/User.hs
new file mode 100644
index 0000000..9064d36
--- /dev/null
+++ b/src/Data/User.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE EmptyDataDecls #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE TypeOperators #-}
+
+
+module Data.User where
+
+import Database.Persist.TH
+import Data.Text
+
+----------------------------------------------------------------------------------------------------
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+User
+ ident Text
+ password Text
+ displayName Text Maybe -- Figure snake_case out
+ avatarUrl Text Maybe -- Figure snake_case out
+ UniqueName ident
+ deriving Show
+|]
diff --git a/src/Data/VersionsData.hs b/src/Data/VersionsData.hs
deleted file mode 100644
index fe41763..0000000
--- a/src/Data/VersionsData.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Data.VersionsData (Versions, makeVersions) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-
-data Versions = Versions -- unstable_features missing
- { versions :: [String]
- } deriving (Eq, Show, Generic)
-
-instance FromJSON Versions where
-instance ToJSON Versions where
-
-makeVersions :: [String] -> Versions
-makeVersions = Versions
diff --git a/src/Data/WellKnownClientData.hs b/src/Data/WellKnownClientData.hs
deleted file mode 100644
index 310002d..0000000
--- a/src/Data/WellKnownClientData.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Data.WellKnownClientData (WellKnownClient, makeWellKnownClient) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-
-data BaseUrlHolder = BaseUrlHolder
- { base_url :: String
- } deriving (Eq, Show, Generic)
-
-instance FromJSON BaseUrlHolder where
-instance ToJSON BaseUrlHolder where
-
-data WellKnownClient = WellKnownClient
- { homeserver :: BaseUrlHolder
- , identity_server :: BaseUrlHolder
- } deriving (Eq, Show, Generic)
-
-fieldMapping :: String -> String
-fieldMapping "homeserver" = "m.homeserver"
-fieldMapping "identity_server" = "m.identity_server"
-fieldMapping name = name
-
-instance FromJSON WellKnownClient where
- parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fieldMapping }
-
-instance ToJSON WellKnownClient where
- toJSON = genericToJSON defaultOptions { fieldLabelModifier = fieldMapping }
-
-makeWellKnownClient :: String -> String -> WellKnownClient
-makeWellKnownClient homeserver identity_server =
- WellKnownClient (BaseUrlHolder homeserver) (BaseUrlHolder identity_server)
diff --git a/src/Data/WellKnownSupportData.hs b/src/Data/WellKnownSupportData.hs
deleted file mode 100644
index 94e434c..0000000
--- a/src/Data/WellKnownSupportData.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Data.WellKnownSupportData (WellKnownSupport, makeWellKnownSupport) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import GHC.Generics (Generic)
-import Data.ContactData
-
-type URI = String
-
-data WellKnownSupport = WellKnownSupport
- { contacts :: [Contact]
- , support_page :: URI
- } deriving (Eq, Show, Generic)
-
-instance FromJSON WellKnownSupport where
-instance ToJSON WellKnownSupport where
-
-makeWellKnownSupport :: [Contact] -> URI -> WellKnownSupport
-makeWellKnownSupport = WellKnownSupport
diff --git a/src/Database.hs b/src/Database.hs
new file mode 100644
index 0000000..1bc7a6b
--- /dev/null
+++ b/src/Database.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Database (runDb, runMigrations) where
+
+import Control.Monad.Logger (runStderrLoggingT, LoggingT)
+import Database.Persist.Sqlite (createSqlitePool, SqlBackend, runSqlPool, runMigration)
+import Control.Monad.Reader (ReaderT)
+import Data.User (migrateAll)
+
+-- | Run a database query in the IO monad.
+runDb :: ReaderT SqlBackend (LoggingT IO) a -> IO a
+runDb query = runStderrLoggingT $ do
+ pool <- createSqlitePool "jamaa.db" 10
+ runSqlPool query pool
+
+-- | Run database migrations.
+runMigrations :: IO ()
+runMigrations = runDb $ runMigration migrateAll
diff --git a/src/Endpoints/APILib.hs b/src/Endpoints/APILib.hs
index 9c89492..1db3f47 100644
--- a/src/Endpoints/APILib.hs
+++ b/src/Endpoints/APILib.hs
@@ -4,8 +4,14 @@
module Endpoints.APILib (API) where
import Servant
-import Endpoints.WellKnownClientEndpoint
-import Endpoints.WellKnownSupportEndpoint
+
+import Endpoints.WellKnownEndpoint
import Endpoints.VersionsEndpoint
+import Endpoints.LoginEndpoint
+import Endpoints.ProfileEndpoint
-type API = WellKnownClientAPI :<|> WellKnownSupportAPI :<|> VersionsAPI
+----------------------------------------------------------------------------------------------------
+type API = WellKnownAPI
+ :<|> VersionsAPI
+ :<|> LoginAPI
+ :<|> ProfileAPI
diff --git a/src/Endpoints/LoginEndpoint.hs b/src/Endpoints/LoginEndpoint.hs
new file mode 100644
index 0000000..ad4a514
--- /dev/null
+++ b/src/Endpoints/LoginEndpoint.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+module Endpoints.LoginEndpoint (LoginAPI, loginServer) where
+
+import Servant
+import Data.Aeson
+import Database (runDb)
+import Database.Persist
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Crypto.BCrypt (validatePassword)
+import qualified Data.ByteString.Base16 as Base16
+
+import Model.Login
+import Model.MatrixErrorResponse
+import Data.User
+
+----------------------------------------------------------------------------------------------------
+type LoginAPI = GetLogin :<|> PostLogin
+
+loginServer :: Server LoginAPI
+loginServer = handleLoginGet :<|> handleLoginPost
+
+--- GET /_matrix/client/v3/login -------------------------------------------------------------------
+type GetLogin = "_matrix" :> "client" :> "v3" :> "login"
+ :> Get '[JSON] LoginFlowsResponse
+
+handleLoginGet :: Handler LoginFlowsResponse
+handleLoginGet = return $ LoginFlowsResponse [LoginFlow "m.login.password"]
+
+--- POST /_matrix/client/v3/login ------------------------------------------------------------------
+type PostLogin = "_matrix" :> "client" :> "v3" :> "login" :> ReqBody '[JSON] LoginRequest
+ :> Post '[JSON] LoginResponse
+
+handleLoginPost :: LoginRequest -> Handler LoginResponse
+handleLoginPost req = do
+ let
+ input_password = password req
+ username = T.unpack $ user $ identifier req
+
+ maybe_user <- liftIO $ runDb $ getBy $ UniqueName $ T.pack username
+
+ case maybe_user of
+ Just (Entity _ db_user) -> do
+ case Base16.decode $ T.encodeUtf8 $ userPassword db_user of
+ Right hashedPasswordBytes ->
+ if validatePassword hashedPasswordBytes (T.encodeUtf8 input_password)
+ then return $ LoginResponse
+ { user_id = T.pack ("@" ++ username ++ ":localhost")
+ , access_token = T.pack ("dummy" ++ username)
+ , home_server = "localhost"
+ , device_id = "DUMMY"
+ }
+ else throwError $ err403 { errBody = encode invalid_credentials_error }
+ Left _ -> throwError $ err500 { errBody = encode password_decoding_error }
+ _ -> throwError $ err403 { errBody = encode invalid_username_error }
+ where
+ invalid_username_error = MatrixErrorResponse "M_FORBIDDEN" "Invalid username"
+ password_decoding_error = MatrixErrorResponse "M_UNKNOWN" "Password hash decoding failed"
+ invalid_credentials_error = MatrixErrorResponse "M_FORBIDDEN" "Invalid username or password" \ No newline at end of file
diff --git a/src/Endpoints/ProfileEndpoint.hs b/src/Endpoints/ProfileEndpoint.hs
new file mode 100644
index 0000000..8aaaa52
--- /dev/null
+++ b/src/Endpoints/ProfileEndpoint.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Endpoints.ProfileEndpoint (ProfileAPI, profileServer) where
+
+import Servant
+import Data.Aeson
+import Data.Text (Text, pack)
+import qualified Data.Text as T
+import Database.Persist
+import Database (runDb)
+import Control.Monad.IO.Class (liftIO)
+
+import Data.User (User(..), Unique(UniqueName))
+import Model.Profile
+import Model.MatrixErrorResponse
+
+----------------------------------------------------------------------------------------------------
+type ProfileAPI = GetProfile
+
+profileServer :: Server ProfileAPI
+profileServer = handleProfileGet
+
+--- GET /_matrix/client/v3/profile/{userId} --------------------------------------------------------
+type GetProfile = "_matrix" :> "client" :> "v3" :> "profile" :> Capture "userId" Text
+ :> Get '[JSON] ProfileResponse
+
+handleProfileGet :: Text -> Handler ProfileResponse
+handleProfileGet user_id = do
+ let username = T.takeWhile (/= ':') $ T.drop 1 user_id
+ maybe_user <- liftIO $ runDb $ getBy $ UniqueName username
+
+ case maybe_user of
+ Just (Entity _ db_user) ->
+ return $ ProfileResponse
+ { display_name = (userDisplayName db_user) <> (Just $ userIdent $ db_user)
+ , avatar_url = userAvatarUrl db_user
+ , tz = Nothing
+ }
+ Nothing ->
+ throwError err404 { errBody = encode user_not_found_error }
+ where
+ user_not_found_error = (MatrixErrorResponse (pack "M_NOT_FOUND") (pack "Profile not found")) \ No newline at end of file
diff --git a/src/Endpoints/ServerLib.hs b/src/Endpoints/ServerLib.hs
index cae6928..7bcf72a 100644
--- a/src/Endpoints/ServerLib.hs
+++ b/src/Endpoints/ServerLib.hs
@@ -2,11 +2,17 @@
module Endpoints.ServerLib (server) where
-import Endpoints.WellKnownClientEndpoint
-import Endpoints.WellKnownSupportEndpoint
+import Servant
+
+import Endpoints.WellKnownEndpoint
import Endpoints.APILib
import Endpoints.VersionsEndpoint
-import Servant
+import Endpoints.LoginEndpoint
+import Endpoints.ProfileEndpoint
+----------------------------------------------------------------------------------------------------
server :: Server API
-server = handleWellKnownClient :<|> handleWellKnownSupport :<|> handleVersions
+server = wellKnownServer
+ :<|> versionsServer
+ :<|> loginServer
+ :<|> profileServer
diff --git a/src/Endpoints/VersionsEndpoint.hs b/src/Endpoints/VersionsEndpoint.hs
index 79fd3ea..e5739cf 100644
--- a/src/Endpoints/VersionsEndpoint.hs
+++ b/src/Endpoints/VersionsEndpoint.hs
@@ -1,12 +1,22 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
-module Endpoints.VersionsEndpoint (VersionsAPI, handleVersions) where
+module Endpoints.VersionsEndpoint (VersionsAPI, versionsServer) where
import Servant
-import Data.VersionsData
+import Data.Text (pack)
-type VersionsAPI = "_matrix" :> "client" :> "versions" :> Get '[JSON] Versions
+import Model.Versions
-handleVersions :: Handler Versions
-handleVersions = return (makeVersions ["1.12"])
+----------------------------------------------------------------------------------------------------
+type VersionsAPI = GetVersions
+
+versionsServer :: Server VersionsAPI
+versionsServer = handleVersionsGet
+-- http://localhost:8080
+--- GET /_matrix/client/versions -------------------------------------------------------------------
+type GetVersions = "_matrix" :> "client" :> "versions"
+ :> Get '[JSON] VersionsResponse
+
+handleVersionsGet :: Handler VersionsResponse
+handleVersionsGet = return $ VersionsResponse [pack "v1.12"]
diff --git a/src/Endpoints/WellKnownClientEndpoint.hs b/src/Endpoints/WellKnownClientEndpoint.hs
deleted file mode 100644
index d091b1d..0000000
--- a/src/Endpoints/WellKnownClientEndpoint.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeOperators #-}
-
-module Endpoints.WellKnownClientEndpoint (WellKnownClientAPI, handleWellKnownClient) where
-
-import Servant
-import Data.WellKnownClientData
-
-type WellKnownClientAPI = ".well-known" :> "matrix" :> "client" :> Get '[JSON] WellKnownClient
-
-handleWellKnownClient :: Handler WellKnownClient
-handleWellKnownClient = return (makeWellKnownClient "http://localhost:8080" "http://localhost:8080")
diff --git a/src/Endpoints/WellKnownEndpoint.hs b/src/Endpoints/WellKnownEndpoint.hs
new file mode 100644
index 0000000..c63c5e5
--- /dev/null
+++ b/src/Endpoints/WellKnownEndpoint.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+
+module Endpoints.WellKnownEndpoint (WellKnownAPI, wellKnownServer) where
+
+import Servant
+import Data.Text (pack)
+
+import Model.WellKnown
+
+----------------------------------------------------------------------------------------------------
+
+type WellKnownAPI = GetWellKnownClient :<|> GetWellKnownSupport
+
+wellKnownServer :: Server WellKnownAPI
+wellKnownServer = handleWellKnownClientGet :<|> handleWellKnownSupportGet
+--- GET /.well-known/matrix/client -----------------------------------------------------------------
+
+type GetWellKnownClient = ".well-known" :> "matrix" :> "client"
+ :> Get '[JSON] WellKnownClientResponse
+
+handleWellKnownClientGet :: Handler WellKnownClientResponse
+handleWellKnownClientGet = return $ WellKnownClientResponse
+ { homeserver = BaseUrlHolder (pack "http://localhost:8080")
+ , identity_server = BaseUrlHolder (pack "https://vector.im")
+ }
+
+--- GET /.well-known/matrix/support ----------------------------------------------------------------
+type GetWellKnownSupport = ".well-known" :> "matrix" :> "support"
+ :> Get '[JSON] WellKnownSupportResponse
+
+handleWellKnownSupportGet :: Handler WellKnownSupportResponse
+handleWellKnownSupportGet = return $ WellKnownSupportResponse
+ { contacts = [Contact (pack "email") (pack "mxid") (pack "m.role.admin")]
+ , support_page = pack "http://localhost:8080/support.html"
+ }
+
diff --git a/src/Endpoints/WellKnownSupportEndpoint.hs b/src/Endpoints/WellKnownSupportEndpoint.hs
deleted file mode 100644
index b6104ae..0000000
--- a/src/Endpoints/WellKnownSupportEndpoint.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeOperators #-}
-
-module Endpoints.WellKnownSupportEndpoint (WellKnownSupportAPI, handleWellKnownSupport) where
-
-import Servant
-import Data.WellKnownSupportData
-import Data.ContactData
-
-type WellKnownSupportAPI = ".well-known" :> "matrix" :> "support" :> Get '[JSON] WellKnownSupport
-
-handleWellKnownSupport :: Handler WellKnownSupport
-handleWellKnownSupport =
- return (
- makeWellKnownSupport
- [makeContact "email" "mxid" "m.role.admin"]
- "http://localhost:8080/support.html"
- )
diff --git a/src/Lib_old.hs b/src/Lib_old.hs
deleted file mode 100644
index 7dd1d1c..0000000
--- a/src/Lib_old.hs
+++ /dev/null
@@ -1,325 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE OverloadedLabels #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# OPTIONS_GHC -Wno-all #-}
-
-module Lib_old
- ( startApp
- , app
- , type (:>) -- Syntax for importing type operator
- , type (:<|>)
- ) where
-
-import Data.Aeson
-import Data.Aeson.TH
-import Network.Wai
-import Network.Wai.Handler.Warp
-import Network.Wai.Middleware.RequestLogger (logStdoutDev)
-import Servant
-import RenameUtils (typeFieldModifier, dotFieldModifier, replaceUsername, replaceRoomId)
-import QueryUserLib
-import PostLoginsResponseLib
-
-type JURI = String
-
-data EmptyObj = EmptyObj {} deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''EmptyObj)
-
-data UserInfo = UserInfo
- { displayname :: String
- , avatar_url :: String
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''UserInfo)
-
-data OAuth2InfoHolder = OAuth2InfoHolder
- { issuer :: String
- , account :: String
- } deriving (Eq, Show)
-
-data BaseURLHolder = BaseURLHolder
- { base_url :: JURI
- } deriving (Eq, Show)
-
-data WellKnown = WellKnown
- { m__homeserver :: BaseURLHolder
- , m__identity_server :: BaseURLHolder
- , org__matrix__msc2965__authentication :: OAuth2InfoHolder
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''BaseURLHolder)
-$(deriveJSON defaultOptions ''OAuth2InfoHolder)
-$(deriveJSON defaultOptions { fieldLabelModifier = dotFieldModifier } ''WellKnown)
-
-data Versions = Versions
- { versions :: [String]
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''Versions)
-
-data LoginFlow = LoginFlow
- { type__ :: String
- } deriving (Eq, Show)
-
-data GetLoginsResponse = GetLoginsResponse
- { flows :: [LoginFlow]
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions {fieldLabelModifier = typeFieldModifier} ''LoginFlow)
-$(deriveJSON defaultOptions ''GetLoginsResponse)
-
-$(deriveJSON defaultOptions ''PostLoginsResponse)
-
-type UserId = String
-
-data RegisterResponse = RegisterResponse
- { user_id :: UserId
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''RegisterResponse)
-
-data MASAviableable = MASAviableable
- { available :: Bool
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''MASAviableable)
-
-data Synced = Synced
- { synced :: Bool
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''Synced)
-
-$(deriveJSON defaultOptions ''QueryUser)
-
-data Rooms = Rooms
- { join :: JoinedRooms
- } deriving (Eq, Show)
-
-data JoinedRooms = JoinedRooms
- {slay :: JoinedRoom
- } deriving (Eq, Show)
-
-data JoinedRoom = JoinedRoom
- { timeline :: Timeline
- } deriving (Eq, Show)
-
-data Timeline = Timeline
- { events :: [Message]
- } deriving (Eq, Show)
-
-data Message = Message
- { content :: TextMessageContent
- , event_id :: String
- , origin_server_ts :: String
- , sender :: String
- , type___ :: String
- , state_key :: String
- } deriving (Eq, Show)
-
-data TextMessageContent = TextMessageContent
- { body :: String
- -- , msgtype :: String
- , creator :: String
- , membership :: String
- , join_rule :: String
- } deriving (Eq, Show)
-
-data Sync = Sync
- { next_batch :: String
- , rooms :: Rooms
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''TextMessageContent)
-$(deriveJSON defaultOptions { fieldLabelModifier = typeFieldModifier} ''Message)
-$(deriveJSON defaultOptions ''Timeline)
-$(deriveJSON defaultOptions ''JoinedRoom)
-$(deriveJSON defaultOptions { fieldLabelModifier = replaceRoomId } ''JoinedRooms)
-$(deriveJSON defaultOptions ''Rooms)
-$(deriveJSON defaultOptions ''Sync)
-
-data Keys = Keys
- { one_time_key_counts :: EmptyObj
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''Keys)
--- ⚠️ This is the key import!
-data KeysHolder = KeysHolder
- { username :: [String]
- } deriving (Eq, Show)
-
-data KeysQuery = KeysQuery
- { device_keys :: KeysHolder
- , master_keys :: EmptyObj
- , self_signing_keys :: EmptyObj
- , user_signing_keys :: EmptyObj
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions { fieldLabelModifier = replaceUsername } ''KeysHolder)
-$(deriveJSON defaultOptions ''KeysQuery)
-
-data RoomId = RoomId
- {room_id :: String
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''RoomId)
-
-data GetRooms = GetRooms
- { chunk :: [Message]
- , end :: String
- , start :: String
- } deriving (Eq, Show)
-
-$(deriveJSON defaultOptions ''GetRooms)
-
-type API = ".well-known" :> "matrix" :> "client" :> Get '[JSON] WellKnown
- :<|> "_matrix" :> "client" :> "versions" :> Get '[JSON] Versions
- :<|> "_matrix" :> "client" :> "v3" :> "login" :> Get '[JSON] GetLoginsResponse
- :<|> "_matrix" :> "client" :> "r0" :> "login" :> Get '[JSON] GetLoginsResponse
- :<|> "_matrix" :> "client" :> "v3" :> "login" :> Post '[JSON] PostLoginsResponse
- :<|> "_matrix" :> "client" :> "r0" :> "login" :> Post '[JSON] PostLoginsResponse
- :<|> "_matrix" :> "client" :> "v3" :> "register" :> Post '[JSON] RegisterResponse
- :<|> "_synapse" :> "mas" :> "is_localpart_available" :> Get '[JSON] MASAviableable
- :<|> "_synapse" :> "mas" :> "provision_user" :> Post '[JSON] RegisterResponse
- :<|> "_synapse" :> "mas" :> "sync_devices" :> Post '[JSON] Synced
- :<|> "_synapse" :> "mas" :> "query_user" :> Get '[JSON] QueryUser
- :<|> "_matrix" :> "client" :> "v3" :> "profile" :> "@username:localhost:8080" :> Get '[JSON] UserInfo
- :<|> "_matrix" :> "client" :> "r0" :> "profile" :> "@username:localhost:8080" :> Get '[JSON] UserInfo
- :<|> "_matrix" :> "client" :> "v3" :> "sync" :> Get '[JSON] Sync
- :<|> "_matrix" :> "client" :> "r0" :> "sync" :> Get '[JSON] Sync
- :<|> "_matrix" :> "client" :> "v3" :> "keys" :> "query" :> Post '[JSON] Keys
- :<|> "_matrix" :> "client" :> "r0" :> "keys" :> "query" :> Post '[JSON] Keys
- :<|> "_matrix" :> "client" :> "v3" :> "keys" :> "upload" :> Post '[JSON] KeysQuery
- :<|> "_matrix" :> "client" :> "r0" :> "keys" :> "upload" :> Post '[JSON] KeysQuery
- :<|> "_matrix" :> "client" :> "v3" :> "createRoom" :> Post '[JSON] RoomId
- :<|> "_matrix" :> "client" :> "r0" :> "createRoom" :> Post '[JSON] RoomId
- :<|> "_matrix" :> "client" :> "v3" :> "rooms" :> "!slay:localhost:8080" :> "messages" :> Get '[JSON] GetRooms
- :<|> "_matrix" :> "client" :> "r0" :> "rooms" :> "!slay:localhost:8080" :> "messages" :> Get '[JSON] GetRooms
- :<|> "_matrix" :> "client" :> "v3" :> "rooms" :> "!slay:localhost:8080" :> "members" :> Get '[JSON] GetRooms
- :<|> "_matrix" :> "client" :> "r0" :> "rooms" :> "!slay:localhost:8080" :> "members" :> Get '[JSON] GetRooms
-
-startApp :: IO ()
-startApp = run 8080 (logStdoutDev app)
-
-app :: Application
-app = serve api server
-
-api :: Proxy API
-api = Proxy
-
-server :: Server API
-server = wellKnownSuccess
- :<|> versionsSuccess
- :<|> getLoginSuccess
- :<|> getLoginSuccess
- :<|> postLoginSuccess
- :<|> postLoginSuccess
- :<|> registerResponse
- :<|> masLocalpartAviableable
- :<|> masProvision
- :<|> masSynced
- :<|> masQueryUser
- :<|> getProfile
- :<|> getProfile
- :<|> sync
- :<|> sync
- :<|> keys
- :<|> keys
- :<|> keysQuery
- :<|> keysQuery
- :<|> createRoom
- :<|> createRoom
- :<|> getRoomContent
- :<|> getRoomContent
- :<|> getRoomMembers
- :<|> getRoomMembers
-
-wellKnownSuccess :: Handler WellKnown
-wellKnownSuccess = return (WellKnown
- (BaseURLHolder "http://localhost:8080")
- (BaseURLHolder "https://vector.im")
- (OAuth2InfoHolder "http://localhost:8000/" "http://localhost:8000/account/")
- )
-
-versionsSuccess :: Handler Versions
-versionsSuccess = return (Versions ["1.10"])
-
-getLoginSuccess :: Handler GetLoginsResponse
-getLoginSuccess = return (GetLoginsResponse [LoginFlow "m.login.sso"])
-
-postLoginSuccess :: Handler PostLoginsResponse
-postLoginSuccess = return (makePostLoginsResponse "token" "tammy" "@username:localhost:8080")
-
-registerResponse :: Handler RegisterResponse
-registerResponse = return (RegisterResponse "@username:localhost:8080")
-
-masLocalpartAviableable :: Handler MASAviableable
-masLocalpartAviableable = return (MASAviableable True)
-
-masProvision :: Handler RegisterResponse
-masProvision = return (RegisterResponse "@username:localhost:8080")
-
-masSynced :: Handler Synced
-masSynced = return (Synced True)
-
-masQueryUser :: Handler QueryUser
-masQueryUser = return (makeQueryUser "username" "@username:localhost:8080" "username" False)
-
-getProfile :: Handler UserInfo
-getProfile = return (UserInfo "username" "mxc://matrix.org/SDGdghriugerRg")
-
-getMessageContentTempl :: String -> String -> String -> String -> Message
-getMessageContentTempl msgtype eventId time state_key = (Message
- (
- TextMessageContent
- "Hiii"
- -- "m.text"
- "@username:localhost:8000"
- "join"
- "public"
- )
- eventId
- time
- "@username:localhost:8000"
- msgtype
- state_key
- )
-
-messages :: [Message]
-messages = [
- getMessageContentTempl "m.room.create" "$make" "1757771812" "",
- -- getMessageContentTempl "m.room.join_rules",
- getMessageContentTempl "m.room.member" "$join" "1757771814" "@username:localhost:8000"
- -- getMessageContentTempl "m.room.message" "$msg" "1757771816" ""
- ]
-
-sync :: Handler Sync
-sync = return (Sync
- "a"
- (Rooms (
- JoinedRooms (
- JoinedRoom (
- Timeline messages
- )
- )
- ))
- )
-
-
-keys :: Handler Keys
-keys = return (Keys EmptyObj)
-
-keysQuery :: Handler KeysQuery
-keysQuery = return (KeysQuery (KeysHolder []) EmptyObj EmptyObj EmptyObj)
-
-createRoom :: Handler RoomId
-createRoom = return (RoomId "!slay:localhost:8080")
-
-getRoomContent :: Handler GetRooms
-getRoomContent = return (GetRooms messages "a" "a")
-
-getRoomMembers :: Handler GetRooms
-getRoomMembers = return (GetRooms [getMessageContentTempl "m.room.member" "$join" "1757771814" "@username:localhost:8080"] "a" "a")
diff --git a/src/Model/Authentication.hs b/src/Model/Authentication.hs
new file mode 100644
index 0000000..be1337a
--- /dev/null
+++ b/src/Model/Authentication.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Model.Authentication (AuthenticationResponse (..)) where
+
+import GHC.Generics
+import Data.Text (Text)
+
+----------------------------------------------------------------------------------------------------
+data AuthenticationResponse = AuthenticationResponse -- delete?
+ { userId :: Text
+ }
+ deriving (Show, Eq, Generic)
diff --git a/src/Model/Login.hs b/src/Model/Login.hs
new file mode 100644
index 0000000..0fbb2fa
--- /dev/null
+++ b/src/Model/Login.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+
+module Model.Login (LoginRequest (..), LoginResponse (..), LoginFlowsResponse (..), LoginFlow (..), PasswordIdentifier (..)) where
+
+import Data.Aeson
+import GHC.Generics
+import Data.Text (Text)
+
+import Util (Normalisable(..))
+
+type UserId = Text
+
+----------------------------------------------------------------------------------------------------
+data PasswordIdentifier = PasswordIdentifier
+ { user :: UserId
+ }
+ deriving (Show, Eq, Generic)
+
+instance FromJSON PasswordIdentifier
+
+data LoginRequest = LoginRequest
+ { identifier :: PasswordIdentifier
+ , password :: Text
+ , type' :: Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance FromJSON LoginRequest where
+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = normaliseVariant }
+
+----------------------------------------------------------------------------------------------------
+data LoginResponse = LoginResponse
+ { user_id :: UserId
+ , access_token :: Text
+ , home_server :: Text
+ , device_id :: Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance ToJSON LoginResponse
+
+----------------------------------------------------------------------------------------------------
+newtype LoginFlowsResponse = LoginFlowsResponse
+ { flows :: [LoginFlow] -- TODO: Enum?
+ }
+ deriving (Show, Eq, Generic)
+
+instance ToJSON LoginFlowsResponse
+
+newtype LoginFlow = LoginFlow -- TODO: Maybe type LoginFlow = ... easier?
+ { type' :: Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance ToJSON LoginFlow where
+ toJSON (LoginFlow t) = object ["type" .= t]
diff --git a/src/Model/MatrixErrorResponse.hs b/src/Model/MatrixErrorResponse.hs
new file mode 100644
index 0000000..22f5ae5
--- /dev/null
+++ b/src/Model/MatrixErrorResponse.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Model.MatrixErrorResponse (MatrixErrorResponse (..)) where
+
+import Data.Aeson
+import GHC.Generics (Generic)
+import Data.Text (Text)
+
+----------------------------------------------------------------------------------------------------
+data MatrixErrorResponse = MatrixErrorResponse
+ { error_code :: Text -- TODO: Enum?
+ , error :: Text
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON MatrixErrorResponse where
+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = \name ->
+ case name of
+ "error_code" -> "errcode"
+ other -> other
+ }
diff --git a/src/Model/Profile.hs b/src/Model/Profile.hs
new file mode 100644
index 0000000..1a8f201
--- /dev/null
+++ b/src/Model/Profile.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Model.Profile (ProfileResponse (..)) where
+
+import Data.Aeson
+import GHC.Generics
+import Data.Text (Text)
+
+----------------------------------------------------------------------------------------------------
+data ProfileResponse = ProfileResponse
+ { display_name :: Maybe Text
+ , avatar_url :: Maybe Text
+ , tz :: Maybe Text
+ -- TODO: <other properties>
+ } deriving (Show, Eq, Generic)
+
+instance ToJSON ProfileResponse where
+ toJSON = genericToJSON defaultOptions
+ { fieldLabelModifier = \name ->
+ case name of
+ "display_name" -> "displayname"
+ "tz" -> "m.tz"
+ other -> other
+ , omitNothingFields = True
+ }
diff --git a/src/Model/Versions.hs b/src/Model/Versions.hs
new file mode 100644
index 0000000..478d5f3
--- /dev/null
+++ b/src/Model/Versions.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Model.Versions (VersionsResponse (..)) where
+
+import Data.Aeson
+import GHC.Generics (Generic)
+import Data.Text (Text)
+
+----------------------------------------------------------------------------------------------------
+
+data VersionsResponse = VersionsResponse -- TODO: <unstable features>
+ { versions :: [Text]
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON VersionsResponse where
diff --git a/src/Model/WellKnown.hs b/src/Model/WellKnown.hs
new file mode 100644
index 0000000..066c9e3
--- /dev/null
+++ b/src/Model/WellKnown.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Model.WellKnown (WellKnownClientResponse (..)
+ , BaseUrlHolder (..)
+ , WellKnownSupportResponse (..)
+ , Contact (..)
+ ) where
+
+import Data.Aeson
+import GHC.Generics (Generic)
+import Data.Text (Text)
+
+type URI = Text
+type EMail = Text
+type UserId = Text
+
+----------------------------------------------------------------------------------------------------
+data BaseUrlHolder = BaseUrlHolder
+ { base_url :: URI
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON BaseUrlHolder where
+
+data WellKnownClientResponse = WellKnownClientResponse
+ { homeserver :: BaseUrlHolder
+ , identity_server :: BaseUrlHolder
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON WellKnownClientResponse where
+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = \name ->
+ case name of
+ "homeserver" -> "m.homeserver"
+ "identity_server" -> "m.identity_server"
+ other -> other
+ }
+
+----------------------------------------------------------------------------------------------------
+data Contact = Contact
+ { email_address :: EMail
+ , matrix_id :: UserId
+ , role :: Text -- Enum? [m.role.admin, m.role.security]
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON Contact where
+
+data WellKnownSupportResponse = WellKnownSupportResponse
+ { contacts :: [Contact]
+ , support_page :: URI
+ } deriving (Eq, Show, Generic)
+
+instance ToJSON WellKnownSupportResponse where \ No newline at end of file
diff --git a/src/PostLoginsResponseLib.hs b/src/PostLoginsResponseLib.hs
deleted file mode 100644
index 51617b1..0000000
--- a/src/PostLoginsResponseLib.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module PostLoginsResponseLib (PostLoginsResponse, makePostLoginsResponse) where
-
-data PostLoginsResponse = PostLoginsResponse
- { access_token :: String
- , device_id :: String
- , user_id :: String
- } deriving (Eq, Show)
-
-makePostLoginsResponse :: String -> String -> String -> PostLoginsResponse
-makePostLoginsResponse = PostLoginsResponse
diff --git a/src/QueryUserLib.hs b/src/QueryUserLib.hs
deleted file mode 100644
index 5677ac5..0000000
--- a/src/QueryUserLib.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-module QueryUserLib (QueryUser, makeQueryUser) where
-
-data QueryUser = QueryUser
- { localpart :: String
- , user_id :: String
- , display_name :: String
- , is_guest :: Bool
- } deriving (Eq, Show)
-
-makeQueryUser :: String -> String -> String -> Bool -> QueryUser
-makeQueryUser = QueryUser
diff --git a/src/RenameUtils.hs b/src/RenameUtils.hs
deleted file mode 100644
index cdf36d2..0000000
--- a/src/RenameUtils.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module RenameUtils where
-
-import Data.List.Utils (startswith, replace)
-
-typeFieldModifier :: String -> String
-typeFieldModifier "type__" = "type"
-typeFieldModifier "type___" = "type"
-typeFieldModifier name = name
-
-dotFieldModifier :: String -> String
-dotFieldModifier = replace "__" "."
-
-replaceUsername :: String -> String
-replaceUsername "username" = "@username:localhost:8080"
-replaceUsername name = name
-
-replaceRoomId :: String -> String
-replaceRoomId "slay" = "!slay:localhost:8080"
-replaceRoomId name = name
diff --git a/src/Util.hs b/src/Util.hs
new file mode 100644
index 0000000..e33e730
--- /dev/null
+++ b/src/Util.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Util (Normalisable(..)) where
+
+import Data.Maybe (fromMaybe)
+import qualified Data.Text as T
+
+----------------------------------------------------------------------------------------------------
+class Normalisable a where
+ normaliseVariant :: a -> a
+
+instance Normalisable [Char] where
+ normaliseVariant = T.unpack . normaliseVariant . T.pack
+
+instance Normalisable T.Text where
+ normaliseVariant str = fromMaybe str (T.stripSuffix (T.pack "'") str)
+