Skip to content

Commit c0c30c1

Browse files
committed
Replaces StateContent and EventType with GADTs
1 parent 67156a4 commit c0c30c1

File tree

2 files changed

+227
-126
lines changed

2 files changed

+227
-126
lines changed

matrix-client/src/Network/Matrix/Client.hs

Lines changed: 166 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
{-# LANGUAGE ScopedTypeVariables #-}
99
{-# LANGUAGE TypeApplications #-}
1010
{-# LANGUAGE TupleSections #-}
11+
{-# LANGUAGE DataKinds #-}
12+
{-# LANGUAGE PolyKinds #-}
13+
{-# LANGUAGE GADTs #-}
14+
{-# LANGUAGE FlexibleInstances #-}
15+
{-# LANGUAGE FlexibleContexts #-}
16+
{-# LANGUAGE UndecidableInstances #-}
1117

1218
-- | This module contains the client-server API
1319
-- https://matrix.org/docs/spec/client_server/r0.6.1
@@ -39,6 +45,7 @@ module Network.Matrix.Client
3945

4046
-- * Room Events
4147
EventType (..),
48+
EventTypeTag (..),
4249
MRCreate (..),
4350
MRCanonicalAlias (..),
4451
MRGuestAccess (..),
@@ -49,6 +56,7 @@ module Network.Matrix.Client
4956
StateKey (..),
5057
StateEvent (..),
5158
StateContent (..),
59+
Some (..),
5260
getRoomEvent,
5361
getRoomMembers,
5462
getRoomState,
@@ -157,10 +165,9 @@ import qualified Network.URI as URI
157165
import Data.Coerce
158166
import Data.Bifunctor (bimap)
159167
import Data.List (intersperse)
160-
import Data.Aeson.Types (Parser)
161-
import Control.Applicative
162168
import qualified Data.ByteString as B
163169
import qualified Data.ByteString.Lazy as BL
170+
import Data.Foldable
164171

165172
-- $setup
166173
-- >>> import Data.Aeson (decode)
@@ -273,113 +280,177 @@ newtype StateKey = StateKey T.Text
273280
deriving stock Show
274281
deriving newtype FromJSON
275282

276-
newtype EventType = EventType T.Text
283+
data EventType
284+
= RoomCreate
285+
| RoomCanonicalAlias
286+
| RoomGuestAccess
287+
| RoomHistoryVisibility
288+
| RoomName
289+
| RoomTopic
290+
| Other
277291
deriving stock Show
278-
deriving newtype FromJSON
292+
293+
data EventTypeTag et where
294+
RoomCreateType :: EventTypeTag 'RoomCreate
295+
RoomCanonicalAliasType :: EventTypeTag 'RoomCanonicalAlias
296+
RoomGuestAccessType :: EventTypeTag 'RoomGuestAccess
297+
RoomHistoryVisibilityType :: EventTypeTag 'RoomHistoryVisibility
298+
RoomNameType :: EventTypeTag 'RoomName
299+
RoomTopicType :: EventTypeTag 'RoomTopic
300+
OtherType :: T.Text -> EventTypeTag 'Other
301+
302+
class Render a where
303+
render :: a -> T.Text
304+
305+
instance Show (EventTypeTag 'RoomCreate) where
306+
show _ = "RoomCreateType"
307+
308+
instance Show (EventTypeTag 'RoomCanonicalAlias) where
309+
show _ = "RoomCanonicalAliasType"
310+
311+
instance Show (EventTypeTag 'RoomGuestAccess) where
312+
show _ = "RoomGuestAccessType"
313+
314+
instance Show (EventTypeTag 'RoomHistoryVisibility) where
315+
show _ = "RoomHistoryVisibilityType"
316+
317+
instance Show (EventTypeTag 'RoomName) where
318+
show _ = "RoomNameType"
319+
320+
instance Show (EventTypeTag 'RoomTopic) where
321+
show _ = "RoomTopicType"
322+
323+
instance Show (EventTypeTag 'Other) where
324+
show (OtherType txt) = "OtherType " <> show txt
325+
326+
instance Render (EventTypeTag 'RoomCreate) where
327+
render RoomCreateType = "m.room.create"
328+
329+
instance Render (EventTypeTag 'RoomCanonicalAlias) where
330+
render RoomCanonicalAliasType = "m.room.canonical_alias"
331+
332+
instance Render (EventTypeTag 'RoomGuestAccess) where
333+
render RoomGuestAccessType = "m.room.guest_access"
334+
335+
instance Render (EventTypeTag 'RoomHistoryVisibility) where
336+
render RoomHistoryVisibilityType = "m.room.history_visibility"
337+
338+
instance Render (EventTypeTag 'RoomName) where
339+
render RoomNameType = "m.room.name"
340+
341+
instance Render (EventTypeTag 'RoomTopic) where
342+
render RoomTopicType = "m.room.topic"
343+
344+
instance Render (EventTypeTag 'Other) where
345+
render (OtherType txt) = txt
346+
347+
instance FromJSON (EventTypeTag 'RoomCreate) where
348+
parseJSON = withText "et" $ \case
349+
"m.room.create" -> pure $ RoomCreateType
350+
_ -> fail "Unexpected event type"
351+
352+
instance FromJSON (EventTypeTag 'RoomCanonicalAlias) where
353+
parseJSON = withText "et" $ \case
354+
"m.room.canonical_alias" -> pure $ RoomCanonicalAliasType
355+
_ -> fail "Unexpected event type"
356+
357+
instance FromJSON (EventTypeTag 'RoomGuestAccess) where
358+
parseJSON = withText "et" $ \case
359+
"m.room.guest_access" -> pure $ RoomGuestAccessType
360+
_ -> fail "Unexpected event type"
361+
362+
instance FromJSON (EventTypeTag 'RoomHistoryVisibility) where
363+
parseJSON = withText "et" $ \case
364+
"m.room.history_visibility" -> pure $ RoomHistoryVisibilityType
365+
_ -> fail "Unexpected event type"
366+
367+
instance FromJSON (EventTypeTag 'RoomName) where
368+
parseJSON = withText "et" $ \case
369+
"m.room.name" -> pure $ RoomNameType
370+
_ -> fail "Unexpected event type"
371+
372+
instance FromJSON (EventTypeTag 'RoomTopic) where
373+
parseJSON = withText "et" $ \case
374+
"m.room.topic" -> pure $ RoomTopicType
375+
_ -> fail "Unexpected event type"
376+
377+
instance FromJSON (EventTypeTag 'Other) where
378+
parseJSON = withText "et" (pure . OtherType)
279379

280380
data MRCreate = MRCreate { mrcCreator :: UserID, mrcRoomVersion :: Integer }
281381
deriving Show
282382

283-
instance FromJSON MRCreate where
284-
parseJSON = withObject "RoomCreate" $ \o -> do
285-
mrcCreator <- o .: "creator"
286-
mrcRoomVersion <- o .: "room_version"
287-
pure $ MRCreate {..}
288-
289383
newtype MRName = MRName { mrnName :: T.Text }
290384
deriving Show
291385

292-
instance FromJSON MRName where
293-
parseJSON = withObject "RoomName" $ \o ->
294-
MRName <$> (o .: "name")
295-
296386
newtype MRCanonicalAlias = MRCanonicalAlias { mrcAlias :: T.Text }
297387
deriving Show
298388

299-
instance FromJSON MRCanonicalAlias where
300-
parseJSON = withObject "RoomCanonicalAlias" $ \o ->
301-
MRCanonicalAlias <$> (o .: "alias")
302389

303390
newtype MRGuestAccess = MRGuestAccess { mrGuestAccess :: T.Text }
304391
deriving Show
305392

306-
instance FromJSON MRGuestAccess where
307-
parseJSON = withObject "GuestAccess" $ \o ->
308-
MRGuestAccess <$> (o .: "guest_access")
309-
310393
newtype MRHistoryVisibility = MRHistoryVisibility { mrHistoryVisibility :: T.Text }
311394
deriving Show
312395

313-
instance FromJSON MRHistoryVisibility where
314-
parseJSON = withObject "HistoryVisibility" $ \o ->
315-
MRHistoryVisibility <$> (o .: "history_visibility")
316-
317396
newtype MRTopic = MRTopic { mrTopic :: T.Text }
318397
deriving Show
319398

320-
instance FromJSON MRTopic where
321-
parseJSON = withObject "RoomTopic" $ \o ->
322-
MRTopic <$> (o .: "topic")
323-
324-
data StateContent =
325-
StRoomCreate MRCreate
326-
-- | StRoomMember MRMember
327-
-- | StRoomPowerLevels MRPowerLevels
328-
-- | StRoomJoinRules MRJoinRules
329-
| StRoomCanonicalAlias MRCanonicalAlias
330-
| StRoomGuestAccess MRGuestAccess
331-
| StRoomHistoryVisibility MRHistoryVisibility
332-
| StRoomName MRName
333-
| StRoomTopic MRTopic
334-
| StOther Value
335-
--- | StSpaceParent MRSpaceParent
336-
deriving Show
337-
338-
pStRoomCreate :: Value -> Parser StateContent
339-
pStRoomCreate v = StRoomCreate <$> parseJSON v
399+
data StateContent et where
400+
ScRoomCreate :: MRCreate -> StateContent 'RoomCreate
401+
-- ScRoomMember :: MRMember -> StateContent 'RoomMember
402+
-- ScRoomPowerLevels :: MRPowerLevels -> StateContent 'RoomPowerLevels
403+
-- ScRoomJoinRules :: MRJoinRules -> StateContent 'RoomJoinRules
404+
ScRoomCanonicalAlias :: MRCanonicalAlias -> StateContent 'RoomCanonicalAlias
405+
ScRoomGuestAccess :: MRGuestAccess -> StateContent 'RoomGuestAccess
406+
ScRoomHistoryVisibility :: MRHistoryVisibility -> StateContent 'RoomHistoryVisibility
407+
ScRoomName :: MRName -> StateContent 'RoomName
408+
ScRoomTopic :: MRTopic -> StateContent 'RoomTopic
409+
ScOther :: Value -> StateContent 'Other
410+
411+
instance FromJSON (StateContent 'RoomCreate) where
412+
parseJSON = withObject "RoomCreate" $ \o -> do
413+
mrcCreator <- o .: "creator"
414+
mrcRoomVersion <- o .: "room_version"
415+
pure $ ScRoomCreate $ MRCreate {..}
340416

341-
pStRoomCanonicAlias :: Value -> Parser StateContent
342-
pStRoomCanonicAlias v = StRoomCanonicalAlias <$> parseJSON v
417+
instance FromJSON (StateContent 'RoomCanonicalAlias) where
418+
parseJSON = withObject "RoomCanonicalAlias" $ \o ->
419+
ScRoomCanonicalAlias . MRCanonicalAlias <$> (o .: "alias")
343420

344-
pStRoomGuestAccess :: Value -> Parser StateContent
345-
pStRoomGuestAccess v = StRoomGuestAccess <$> parseJSON v
421+
instance FromJSON (StateContent 'RoomGuestAccess) where
422+
parseJSON = withObject "GuestAccess" $ \o ->
423+
ScRoomGuestAccess . MRGuestAccess <$> (o .: "guest_access")
346424

347-
pStRoomHistoryVisibility :: Value -> Parser StateContent
348-
pStRoomHistoryVisibility v = StRoomHistoryVisibility <$> parseJSON v
425+
instance FromJSON (StateContent 'RoomHistoryVisibility) where
426+
parseJSON = withObject "HistoryVisibility" $ \o ->
427+
ScRoomHistoryVisibility . MRHistoryVisibility <$> (o .: "history_visibility")
349428

350-
pStRoomName :: Value -> Parser StateContent
351-
pStRoomName v = StRoomName <$> parseJSON v
429+
instance FromJSON (StateContent 'RoomName) where
430+
parseJSON = withObject "RoomName" $ \o ->
431+
ScRoomName . MRName <$> (o .: "name")
352432

353-
pStRoomTopic :: Value -> Parser StateContent
354-
pStRoomTopic v = StRoomTopic <$> parseJSON v
433+
instance FromJSON (StateContent 'RoomTopic) where
434+
parseJSON = withObject "RoomTopic" $ \o ->
435+
ScRoomTopic . MRTopic <$> (o .: "topic")
355436

356-
pStRoomOther :: Value -> Parser StateContent
357-
pStRoomOther v = StOther <$> parseJSON v
358-
359-
instance FromJSON StateContent where
360-
parseJSON v =
361-
pStRoomCreate v
362-
<|> pStRoomCanonicAlias v
363-
<|> pStRoomGuestAccess v
364-
<|> pStRoomHistoryVisibility v
365-
<|> pStRoomName v
366-
<|> pStRoomTopic v
367-
<|> pStRoomOther v
437+
instance FromJSON (StateContent 'Other) where
438+
parseJSON = pure . ScOther
368439

369440
-- TODO(SOLOMON): Should This constructor be in 'Event'?
370-
data StateEvent = StateEvent
371-
{ seContent :: StateContent
441+
data StateEvent et = StateEvent
442+
{ seContent :: StateContent et
372443
, seEventId :: EventID
373444
, seOriginServerTimestamp :: Integer
374445
, sePreviousContent :: Maybe Value
375446
, seRoomId :: RoomID
376447
, seSender :: UserID
377448
, seStateKey :: StateKey
378-
, seEventType :: EventType
449+
, seEventType :: EventTypeTag et
379450
, seUnsigned :: Maybe Value
380-
} deriving Show
451+
} deriving stock Generic
381452

382-
instance FromJSON StateEvent where
453+
instance (FromJSON (StateContent et), FromJSON (EventTypeTag et)) => FromJSON (StateEvent et) where
383454
parseJSON = withObject "StateEvent" $ \o -> do
384455
seContent <- o .: "content"
385456
seEventId <- fmap EventID $ o .: "event_id"
@@ -394,7 +465,7 @@ instance FromJSON StateEvent where
394465

395466
-- | Get the state events for the current state of a room.
396467
-- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidstate
397-
getRoomState :: ClientSession -> RoomID -> MatrixIO [StateEvent]
468+
getRoomState :: ClientSession -> RoomID -> MatrixIO [Some StateEvent]
398469
getRoomState session (RoomID rid) = do
399470
request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/state"
400471
doRequest session request
@@ -404,9 +475,10 @@ getRoomState session (RoomID rid) = do
404475
-- of the room. If the user has left the room then the state is taken
405476
-- from the state of the room when they left.
406477
-- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey
407-
getRoomStateEvent :: ClientSession -> RoomID -> EventType -> StateKey -> MatrixIO StateEvent
408-
getRoomStateEvent session (RoomID rid) (EventType et) (StateKey key) = do
409-
request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/state" <> et <> "/" <> key
478+
getRoomStateEvent :: (FromJSON (EventTypeTag et), FromJSON (StateContent et), Render (EventTypeTag et)) =>
479+
ClientSession -> RoomID -> (EventTypeTag et) -> StateKey -> MatrixIO (StateEvent et)
480+
getRoomStateEvent session (RoomID rid) et (StateKey key) = do
481+
request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/state/" <> render et <> "/" <> key
410482
doRequest session request
411483

412484
data Dir = F | B
@@ -415,15 +487,28 @@ renderDir :: Dir -> B.ByteString
415487
renderDir F = "f"
416488
renderDir B = "b"
417489

490+
491+
instance FromJSON (Some StateEvent) where
492+
parseJSON val = asum [ Some <$> parseJSON @(StateEvent 'RoomCreate) val
493+
, Some <$> parseJSON @(StateEvent 'RoomCanonicalAlias) val
494+
, Some <$> parseJSON @(StateEvent 'RoomGuestAccess) val
495+
, Some <$> parseJSON @(StateEvent 'RoomHistoryVisibility) val
496+
, Some <$> parseJSON @(StateEvent 'RoomName) val
497+
, Some <$> parseJSON @(StateEvent 'RoomTopic) val
498+
, Some <$> parseJSON @(StateEvent 'Other) val
499+
]
500+
501+
data Some f = forall x. Some (f x)
502+
418503
data PaginatedRoomMessages = PaginatedRoomMessages
419504
{ chunk :: [RoomEvent]
420505
, end :: Maybe T.Text
421506
-- ^ A token corresponding to the end of chunk.
422507
, start :: T.Text
423508
-- ^ A token corresponding to the start of chunk.
424-
, state :: [StateEvent]
509+
, state :: [Some StateEvent]
425510
-- ^ A list of state events relevant to showing the chunk.
426-
} deriving Show
511+
}
427512

428513
instance FromJSON PaginatedRoomMessages where
429514
parseJSON = withObject "PaginatedRoomMessages" $ \o -> do
@@ -461,9 +546,9 @@ getRoomMessages session (RoomID rid) dir roomFilter fromToken limit toToken = do
461546
-- | Send arbitrary state events to a room. These events will be overwritten if
462547
-- <room id>, <event type> and <state key> all match.
463548
-- https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
464-
sendRoomStateEvent :: ClientSession -> RoomID -> EventType -> StateKey -> Value -> MatrixIO EventID
465-
sendRoomStateEvent session (RoomID rid) (EventType et) (StateKey key) event = do
466-
request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> escapeUriComponent rid <> "/state/" <> escapeUriComponent et <> "/" <> escapeUriComponent key
549+
sendRoomStateEvent :: Render (EventTypeTag et) => ClientSession -> RoomID -> EventTypeTag et -> StateKey -> Value -> MatrixIO EventID
550+
sendRoomStateEvent session (RoomID rid) et (StateKey key) event = do
551+
request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> escapeUriComponent rid <> "/state/" <> escapeUriComponent (render et) <> "/" <> escapeUriComponent key
467552
doRequest session $
468553
request { HTTP.method = "PUT"
469554
, HTTP.requestBody = HTTP.RequestBodyLBS $ encode event

0 commit comments

Comments
 (0)