Clean up Types

This commit is contained in:
Joscha 2018-01-27 21:08:07 +00:00
parent 9636e1eb4d
commit ce13ce11fc
3 changed files with 188 additions and 99 deletions

View file

@ -24,12 +24,14 @@ dependencies:
# basic stuff
- time
- text
- transformers
# websocket connection
- websockets
- wuss
# parsing json
- aeson
- bytestring
- unordered-containers
# other
- stm

View file

@ -1,7 +1,61 @@
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | Setup consisting of a few threads to send and receive packets to and from
-- the euphoria api using a websocket connection.
--
-- @
-- m: main thread
-- r: recvThread
-- f: fetchThread
-- s: sendThread
--
-- On creation:
-- m: Create WS connection
-- m: Create channels
-- m: Start recvThread with all necessary info
-- r: Start fetchThread and sendThread using async
-- m: Return SendChan and EventChan
--
-- On disconnect:
-- s: close connection (optional)
-- f: detect exception
-- f: RDisconnected -> RecvChan
-- f: *stops*
-- r: RecvChan -> RDisconnected
-- r: close SendChan
-- s: *stops*
-- r: wait for f and s to stop
-- r: clean up SendChan
-- r: clean up RecvChan
-- r: clean up response list
-- r: EventStopped -> EventChan
-- r: *stops*
-- -> All MVars are dealt with
--
-- ↓
-- │
-- (SendChan)
-- │
-- ┌─────────────────────╴│╶──────┐
-- │ │ │
-- │ (WS.Connection) │ │
-- │ │ │ │
-- │ [fetchThread] [sendThread] │
-- │ │ │ │
-- │ └──────┬──────┘ │
-- │ │ │
-- │ (RecvChan) │
-- │ │ │
-- │ [recvThread] │
-- │ │ │
-- └──────────────╴│╶─────────────┘
-- │
-- (EventChan)
-- │
-- ↓
-- @
module EuphApi.Threads (
-- * Events and replies
@ -13,22 +67,55 @@ module EuphApi.Threads (
import Control.Concurrent
import Control.Exception
import Data.Aeson
import Data.Text
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.State
import Data.Aeson as A
import qualified Data.ByteString.Lazy as BS
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified EuphApi.CloseableChan as E
import qualified EuphApi.Types as E
import qualified Network.WebSockets as WS
-- Some useful type aliases
type PacketID = Text
{-
- Events and replies
- Some stuff
-}
-- Some useful type aliases
type PacketID = T.Text
type Reply = Either Failure
-- | The ways in which getting a reply from the server can fail.
data Failure = FailDisconnect -- ^ Disconnected from the server while waiting for the reply.
| FailError T.Text -- ^ The server replied with an error.
| FailParse -- ^ Could not parse the server's reply correctly.
class ToJSONObject a where
toJSONObject :: a -> Object
(.?=) :: (ToJSON v, KeyValue kv) => T.Text -> Maybe v -> [kv]
k .?= (Just v) = [k .= v]
k .?= Nothing = []
packetOfType :: T.Text -> Value -> Object
packetOfType packetType packetData =
HM.fromList [("type", A.String packetType), ("data", packetData)]
{-
- Commands
-}
data SendCommand = SendCommand
{ sendCommandContent :: T.Text
, sendCommandParent :: Maybe E.Snowflake
} deriving (Show)
instance ToJSONObject SendCommand where
toJSONObject (SendCommand{..}) =
let obj = object $ ["content" .= sendCommandContent] ++ ("parent" .?= sendCommandParent)
in packetOfType "data" obj
-- send-reply
data SendReply = SendReply
{ sendReplyMessage :: E.Message
@ -41,94 +128,95 @@ instance FromJSON SendReply where
- API functions
-}
send :: SendChan -> Text -> IO (Either Failure E.Message)
send :: SendChan -> T.Text -> IO (Reply E.Message)
send = undefined
reply :: SendChan -> PacketID -> Text -> IO (Either Failure E.Message)
reply :: SendChan -> PacketID -> T.Text -> IO (Reply E.Message)
reply = undefined
{-
- Channels
-}
type RecvChan = E.CloseableChan Recv
data Recv = RDisconnected
| RPacket BS.ByteString
| forall a . (FromJSON a) => RReply PacketID (MVar (Reply a))
type SendChan = E.CloseableChan Send
data Send = SDisconnect
| forall p . (ToJSONObject p) => SNoReply p
| forall p r . (ToJSONObject p, FromJSON r) => SReply p (MVar (Reply r))
type EventChan e = E.CloseableChan (Event e)
data Event e = EDisconnected
| EStopped
| EEuphEventPlaceholder
| ECustomEvent e
{-
data Packet = Packet
{ packetID :: Maybe PacketID
, packetType :: Text
, packetContent :: Content
, packetThrottled :: Maybe Text
}
-}
- Fetch thread
-}
type SendChan = Chan Send
-- Contents of sendChan
data Send = SPacket Text --Value -- packet type, content
| SDisconnect
type RecvChan = Chan Recv
-- Contents of recvChan
data Recv = RConnectionClosed -- Ws connection closed
-- | RPacket ByteString -- Packet received from the ws connection
-- | forall c . (FromJSON c) => RReply PacketID (MVar (Response c)) -- Request for a reply with a certain ID
{-
sendPacket :: Connection -> Packet -> IO ()
sendPacket = undefined
recvPacket :: Connection -> IO Packet
recvPacket = undefined
sendThread :: SendChan -> RecvChan -> Connection -> IO ()
sendThread s r c = do
return ()
type EventChan = Chan Event
-- Contents of eventChan
data Event = EPlaceholder
fetchMessage :: RecvChan -> Connection -> IO ()
fetchMessage recv con = do
message <- receiveData con
writeChan recv (RPacket message)
fetchMessage recv con
fetchThread :: RecvChan -> Connection -> IO ()
fetchThread recv con = fetchMessage recv con `catch` handleException
fetchThread :: RecvChan -> WS.Connection -> IO ()
fetchThread cRecv con = handle handleException $ forever $ do
message <- WS.receiveData con
void $ E.writeChan cRecv (RPacket message) -- will never be closed while thread running
where
handleException (CloseRequest _ _) = writeChan recv RConnectionClosed
handleException ConnectionClosed = writeChan recv RConnectionClosed
handleException _ = fetchThread recv con
handleException (WS.CloseRequest _ _) = void $ E.writeChan cRecv RDisconnected
handleException WS.ConnectionClosed = void $ E.writeChan cRecv RDisconnected
handleException _ = fetchThread cRecv con
sendMessage :: SendChan -> RecvChan -> Connection -> IO ()
sendMessage send recv con = do
message <- readChan send
{-
- Send thread
-}
type SendState = StateT Integer IO
-- Prepare a single packet for sending
preparePacket :: (ToJSONObject p) => p -> SendState (BS.ByteString, PacketID)
preparePacket packet = do
packetNr <- get
put $ packetNr + 1
let packetID = T.pack $ show packetNr
obj = HM.insert "id" (A.String packetID) $ toJSONObject packet
bytestr = encode $ Object obj
return (bytestr, packetID)
sendThread :: SendChan -> RecvChan -> WS.Connection -> SendState ()
sendThread cSend cRecv con = do
item <- liftIO $ E.readChan cSend
case item of
Nothing -> do
return ()
sendThread :: SendChan -> RecvChan -> Connection -> IO ()
sendThread = undefined
-}
Just SDisconnect -> do
liftIO $ WS.sendClose con ("Bye." :: T.Text)
Just (SNoReply value) -> do
(packet, _) <- preparePacket value
liftIO $ WS.sendTextData con packet
continue <- liftIO $ sendSafely packet
if continue
then sendThread cSend cRecv con
else return ()
Just (SReply value reply) -> do
(packet, packetID) <- preparePacket value
liftIO $ E.writeChan cRecv $ RReply packetID reply
continue <- liftIO $ sendSafely packet
if continue
then sendThread cSend cRecv con
else return ()
where
sendSafely packet = (WS.sendTextData con packet >> return True) `catch` handleException
handleException (WS.CloseRequest _ _) = return False
handleException WS.ConnectionClosed = return False
handleException _ = return True
{-
- RecvThread
-}
-- TODO

View file

@ -1,11 +1,8 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | This module implements parts of the Euphoria API at
-- | This module implements a few types from the Euphoria API at
-- <http://api.euphoria.io/#overview>.
--
-- Currently, accounts are not implemented.
-- This means that all account, room host and staff commands are not implemented.
module EuphApi.Types
( Snowflake
@ -23,7 +20,8 @@ import Data.Time
-- | Represents <http://api.euphoria.io/#snowflake>.
--
-- A 'Snowflake' is a 13-character string, usually used as a unique identifier for some type of object.
-- A 'Snowflake' is a 13-character string, usually used as a unique identifier
-- for some type of object.
-- It is the base-36 encoding of an unsigned, 64-bit integer.
type Snowflake = T.Text
@ -45,8 +43,8 @@ instance FromJSON UserID where
userType = findUserType tp
userSnowflake = T.drop 1 sf
in return $ if userType == Other
then UserID {userSnowflake=t, ..}
else UserID {..}
then UserID{userSnowflake=t, ..}
else UserID{..}
where
findUserType txt
| txt == "account" = Account
@ -67,7 +65,8 @@ data UserType = Agent
-- | Represents <http://api.euphoria.io/#message>.
--
-- A 'Message' is a node in a Rooms Log.
-- It corresponds to a chat message, or a post, or any broadcasted event in a room that should appear in the log.
-- It corresponds to a chat message, or a post, or any broadcasted event in a room
-- that should appear in the log.
--
-- The fields @previous_edit_id@ and @encryption_key_id@ are not implemented.
data Message = Message
@ -108,7 +107,7 @@ instance FromJSON Message where
msgEdited <- o .:? "edited"
msgDeleted <- o .:? "deleted"
msgTruncated <- o .:? "truncated" .!= False
return $ Message {..}
return $ Message{..}
-- | Represents <http://api.euphoria.io/#sessionview>.
--
@ -136,4 +135,4 @@ instance FromJSON SessionView where
sessSessionID <- o .: "session_id"
isStaff <- o .:? "is_staff" .!= False
isManager <- o .:? "is_manager" .!= False
return $ SessionView {..}
return $ SessionView{..}