Rewrite Connection to not depend on CloseableChan
This commit is contained in:
parent
26d08b7312
commit
102010f442
2 changed files with 357 additions and 548 deletions
|
|
@ -1,150 +0,0 @@
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
|
||||||
|
|
||||||
-- | Chans that can be closed and reopened.
|
|
||||||
--
|
|
||||||
-- While a 'CloseableChan' is closed, it can not be written to or read from.
|
|
||||||
-- Calls to 'writeChan' and 'readChan' are non-blocking while a chan is closed.
|
|
||||||
--
|
|
||||||
-- If a thread is attempting to read from a chan using 'readChan' and that chan is closed,
|
|
||||||
-- the call to 'readChan' resumes and @Nothing@ is returned.
|
|
||||||
|
|
||||||
module EuphApi.CloseableChan
|
|
||||||
( CloseableChan
|
|
||||||
-- * IO function versions
|
|
||||||
, newOpenChan
|
|
||||||
, newClosedChan
|
|
||||||
, writeChan
|
|
||||||
, readChan
|
|
||||||
, closeChan
|
|
||||||
, openChan
|
|
||||||
, emptyChan
|
|
||||||
-- * STM function versions
|
|
||||||
, newOpenChanSTM
|
|
||||||
, newClosedChanSTM
|
|
||||||
, writeChanSTM
|
|
||||||
, readChanSTM
|
|
||||||
, closeChanSTM
|
|
||||||
, openChanSTM
|
|
||||||
, emptyChanSTM
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Control.Concurrent.STM
|
|
||||||
|
|
||||||
-- | A 'Chan' that can be closed and opened again.
|
|
||||||
--
|
|
||||||
-- Attempts to write to or read from a 'CloseableChan' while it is closed result
|
|
||||||
-- in a @Nothing@.
|
|
||||||
data CloseableChan a = CloseableChan
|
|
||||||
{ cClosed :: TVar Bool
|
|
||||||
, cChan :: TChan (Content a)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- TODO: Replace with Maybe?
|
|
||||||
data Content a = Value a
|
|
||||||
| End
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Functions as STM actions
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- | See 'newOpenChan'.
|
|
||||||
newOpenChanSTM :: STM (CloseableChan a)
|
|
||||||
newOpenChanSTM = do
|
|
||||||
cClosed <- newTVar False
|
|
||||||
cChan <- newTChan
|
|
||||||
return CloseableChan{..}
|
|
||||||
|
|
||||||
-- | See 'newClosedChan'.
|
|
||||||
newClosedChanSTM :: STM (CloseableChan a)
|
|
||||||
newClosedChanSTM = do
|
|
||||||
cClosed <- newTVar True
|
|
||||||
cChan <- newTChan
|
|
||||||
return CloseableChan{..}
|
|
||||||
|
|
||||||
-- | See 'writeChan'.
|
|
||||||
writeChanSTM :: CloseableChan a -> a -> STM (Maybe ())
|
|
||||||
writeChanSTM CloseableChan{..} a = do
|
|
||||||
closed <- readTVar cClosed
|
|
||||||
if closed
|
|
||||||
then return Nothing
|
|
||||||
else Just <$> writeTChan cChan (Value a)
|
|
||||||
|
|
||||||
-- | See 'readChan'.
|
|
||||||
readChanSTM :: CloseableChan a -> STM (Maybe a)
|
|
||||||
readChanSTM CloseableChan{..} = do
|
|
||||||
closed <- readTVar cClosed
|
|
||||||
if closed
|
|
||||||
then return Nothing
|
|
||||||
else Just <$> readValue
|
|
||||||
where
|
|
||||||
readValue = do
|
|
||||||
val <- readTChan cChan
|
|
||||||
case val of
|
|
||||||
End -> readValue -- ignore End while reading normally
|
|
||||||
Value v -> return v
|
|
||||||
|
|
||||||
-- | See 'closeChan'.
|
|
||||||
closeChanSTM :: CloseableChan a -> STM ()
|
|
||||||
closeChanSTM CloseableChan{..} = writeTVar cClosed True
|
|
||||||
--writeTChan cChan End
|
|
||||||
|
|
||||||
-- | See 'openChan'.
|
|
||||||
openChanSTM :: CloseableChan a -> STM ()
|
|
||||||
openChanSTM CloseableChan{..} = writeTVar cClosed False
|
|
||||||
|
|
||||||
-- | See 'emptyChan'.
|
|
||||||
emptyChanSTM :: CloseableChan a -> STM [a]
|
|
||||||
emptyChanSTM CloseableChan{..} = do
|
|
||||||
writeTChan cChan End
|
|
||||||
extractValues
|
|
||||||
where
|
|
||||||
extractValues = do
|
|
||||||
val <- readTChan cChan
|
|
||||||
case val of
|
|
||||||
End -> return []
|
|
||||||
Value v -> (v :) <$> extractValues
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Functions as IO actions
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- | Create a new open 'CloseableChan'.
|
|
||||||
newOpenChan :: IO (CloseableChan a)
|
|
||||||
newOpenChan = atomically newOpenChanSTM
|
|
||||||
|
|
||||||
-- | Create a new closed 'CloseableChan'.
|
|
||||||
newClosedChan :: IO (CloseableChan a)
|
|
||||||
newClosedChan = atomically newClosedChanSTM
|
|
||||||
|
|
||||||
-- | Attempt to write a value into the 'CloseableChan'.
|
|
||||||
--
|
|
||||||
-- If the chan is open, succeeds with a @Just ()@.
|
|
||||||
-- If the chan is closed, fails with a @Nothing@.
|
|
||||||
writeChan :: CloseableChan a -> a -> IO (Maybe ())
|
|
||||||
writeChan chan a = atomically $ writeChanSTM chan a
|
|
||||||
|
|
||||||
-- | Attempt to read a value @v@ from the 'CloseableChan'.
|
|
||||||
--
|
|
||||||
-- If the chan is open, succeeds with a @Just v@.
|
|
||||||
-- If the chan is closed, fails with a @Nothing@.
|
|
||||||
readChan :: CloseableChan a -> IO (Maybe a)
|
|
||||||
readChan = atomically . readChanSTM
|
|
||||||
|
|
||||||
-- | Close a 'CloseableChan'.
|
|
||||||
-- Does nothing if chan is already closed.
|
|
||||||
--
|
|
||||||
-- Performing this action un-blocks all calls to 'readChan'.
|
|
||||||
closeChan :: CloseableChan a -> IO ()
|
|
||||||
closeChan = atomically . closeChanSTM
|
|
||||||
|
|
||||||
-- | Open a 'CloseableChan'.
|
|
||||||
-- Does nothing if chan is already open.
|
|
||||||
openChan :: CloseableChan a -> IO ()
|
|
||||||
openChan = atomically . openChanSTM
|
|
||||||
|
|
||||||
-- | Remove all items currently in the 'CloseableChan' and returns them in a list.
|
|
||||||
--
|
|
||||||
-- This function also works while the chan is closed.
|
|
||||||
-- It is meant as a way to clean up the remaining values in a chan after it was closed.
|
|
||||||
emptyChan :: CloseableChan a -> IO [a]
|
|
||||||
emptyChan = atomically . emptyChanSTM
|
|
||||||
|
|
@ -5,82 +5,28 @@
|
||||||
|
|
||||||
-- | Setup consisting of a few threads to send and receive packets to and from
|
-- | Setup consisting of a few threads to send and receive packets to and from
|
||||||
-- the euphoria api using a websocket connection.
|
-- the euphoria api using a websocket connection.
|
||||||
--
|
|
||||||
-- @
|
|
||||||
-- m: main thread
|
|
||||||
-- r: recvThread
|
|
||||||
-- f: fetchThread
|
|
||||||
-- s: sendThread
|
|
||||||
--
|
|
||||||
-- On creation:
|
|
||||||
-- m: Create WS connection (or do this in r?)
|
|
||||||
-- 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: EDisconnected -> EventChan
|
|
||||||
-- 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: EStopped -> EventChan
|
|
||||||
-- r: *stops*
|
|
||||||
-- -> All MVars are dealt with
|
|
||||||
--
|
|
||||||
-- ↓
|
|
||||||
-- │
|
|
||||||
-- (SendChan)
|
|
||||||
-- │
|
|
||||||
-- ┌─────────────────────╴│╶──────┐
|
|
||||||
-- │ │ │
|
|
||||||
-- │ (WS.Connection) │ │
|
|
||||||
-- │ │ │ │
|
|
||||||
-- │ [fetchThread] [sendThread] │
|
|
||||||
-- │ │ │ │
|
|
||||||
-- │ └──────┬──────┘ │
|
|
||||||
-- │ │ │
|
|
||||||
-- │ (RecvChan) │
|
|
||||||
-- │ │ │
|
|
||||||
-- │ [recvThread] │
|
|
||||||
-- │ │ │
|
|
||||||
-- └──────────────╴│╶─────────────┘
|
|
||||||
-- │
|
|
||||||
-- (EventChan)
|
|
||||||
-- │
|
|
||||||
-- ↓
|
|
||||||
-- @
|
|
||||||
|
|
||||||
module EuphApi.Threads (
|
module EuphApi.Threads (
|
||||||
-- * Connecting to euphoria
|
-- * Connecting to euphoria
|
||||||
EuphConnection
|
Connection
|
||||||
, euphClient
|
, euphApp
|
||||||
, getEvents
|
, getEvent
|
||||||
-- * API functions
|
-- * API functions
|
||||||
, pingReply
|
, pingReply
|
||||||
, nick
|
, nick
|
||||||
, send
|
, send
|
||||||
-- * Events and replies
|
-- * Exception
|
||||||
, EuphException(..)
|
, EuphException(..)
|
||||||
, EuphEvent(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Control.Concurrent
|
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
import Data.Maybe
|
|
||||||
|
|
||||||
|
import Control.Concurrent
|
||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
|
import Control.Concurrent.STM
|
||||||
import Control.Monad.Trans.State
|
import Control.Monad.Trans.State
|
||||||
import Data.Aeson as A
|
import Data.Aeson as A
|
||||||
import qualified Data.ByteString.Lazy as BS
|
import qualified Data.ByteString.Lazy as BS
|
||||||
|
|
@ -91,14 +37,251 @@ import Data.Time
|
||||||
import Data.Time.Clock.POSIX
|
import Data.Time.Clock.POSIX
|
||||||
import qualified Network.WebSockets as WS
|
import qualified Network.WebSockets as WS
|
||||||
|
|
||||||
import qualified EuphApi.CloseableChan as E
|
|
||||||
import qualified EuphApi.Types as E
|
import qualified EuphApi.Types as E
|
||||||
|
|
||||||
-- Some useful type aliases
|
|
||||||
type PacketID = T.Text
|
type PacketID = T.Text
|
||||||
type Reply = Either EuphException
|
type Reply = Either EuphException
|
||||||
data ReplyMVar = forall r . (FromJSON r) => ReplyMVar (MVar (Reply r))
|
data ReplyMVar = forall r . (FromJSON r) => ReplyMVar (MVar (Reply r))
|
||||||
|
|
||||||
|
type SendQueue = TBQueue Send
|
||||||
|
type RecvQueue = TBQueue Recv
|
||||||
|
type EventQueue = TBQueue (Maybe Event) -- 'Nothing' ends the event stream
|
||||||
|
type LockedFlag = TVar Bool
|
||||||
|
|
||||||
|
data Connection = Connection LockedFlag SendQueue EventQueue
|
||||||
|
|
||||||
|
-- | Read one 'Event' from the 'Connection'.
|
||||||
|
--
|
||||||
|
-- Returns 'Nothing' once when the 'Connection' stops.
|
||||||
|
-- After that, any further calls of getEvent on the same connection
|
||||||
|
-- will block indefinitely.
|
||||||
|
getEvent :: Connection -> IO (Maybe Event)
|
||||||
|
getEvent (Connection locked _ qEvent) = undefined locked qEvent
|
||||||
|
|
||||||
|
-- | A @Network.Websockets.'WS.ClientApp'@ creating a 'Connection'.
|
||||||
|
euphApp :: WS.ClientApp Connection
|
||||||
|
euphApp con = do
|
||||||
|
sendQueue <- atomically $ newTBQueue 10
|
||||||
|
recvQueue <- atomically $ newTBQueue 10
|
||||||
|
eventQueue <- atomically $ newTBQueue 10
|
||||||
|
locked <- atomically $ newTVar False
|
||||||
|
let euphCon = Connection locked sendQueue eventQueue
|
||||||
|
void $ forkIO $ recvThread euphCon recvQueue con
|
||||||
|
return euphCon
|
||||||
|
|
||||||
|
{-
|
||||||
|
- Fetch thread
|
||||||
|
-}
|
||||||
|
|
||||||
|
fetchThread :: RecvQueue -> WS.Connection -> IO ()
|
||||||
|
fetchThread qRecv con = void $ handle handleException $ forever $ do
|
||||||
|
message <- WS.receiveData con
|
||||||
|
void $ atomically $ writeTBQueue qRecv (RPacket message)
|
||||||
|
where
|
||||||
|
handleException (WS.CloseRequest _ _) = atomically $ writeTBQueue qRecv RDisconnected
|
||||||
|
handleException WS.ConnectionClosed = atomically $ writeTBQueue qRecv RDisconnected
|
||||||
|
handleException _ = fetchThread qRecv con
|
||||||
|
|
||||||
|
{-
|
||||||
|
- Send thread
|
||||||
|
-}
|
||||||
|
|
||||||
|
-- Prepare a single packet for sending.
|
||||||
|
-- Doesn't actually do any IO. The IO monad part is left in for ease of use.
|
||||||
|
preparePacket :: (ToJSON p) => T.Text -> p -> StateT Integer IO (BS.ByteString, PacketID)
|
||||||
|
preparePacket packetType packetData = do
|
||||||
|
packetNr <- get
|
||||||
|
put $ packetNr + 1
|
||||||
|
let packetID = T.pack $ show packetNr
|
||||||
|
bytestr = encode . Object . HM.fromList $
|
||||||
|
[ ("id", A.String packetID)
|
||||||
|
, ("type", A.String packetType)
|
||||||
|
, ("data", toJSON packetData)
|
||||||
|
]
|
||||||
|
return (bytestr, packetID)
|
||||||
|
|
||||||
|
readWhileOpen :: Connection -> STM (Maybe Send)
|
||||||
|
readWhileOpen (Connection locked qSend _) = do
|
||||||
|
isLocked <- readTVar locked
|
||||||
|
if isLocked
|
||||||
|
then return Nothing
|
||||||
|
else Just <$> readTBQueue qSend
|
||||||
|
|
||||||
|
sendThread :: Connection -> RecvQueue -> WS.Connection -> StateT Integer IO ()
|
||||||
|
sendThread euphCon qRecv con = do
|
||||||
|
item <- liftIO $ atomically $ readWhileOpen euphCon
|
||||||
|
case item of
|
||||||
|
Nothing ->
|
||||||
|
return ()
|
||||||
|
|
||||||
|
Just SDisconnect ->
|
||||||
|
liftIO $ WS.sendClose con ("Bye." :: T.Text)
|
||||||
|
|
||||||
|
Just (SNoReply packetType packetData) -> do
|
||||||
|
(packet, _) <- preparePacket packetType packetData
|
||||||
|
liftIO $ WS.sendTextData con packet
|
||||||
|
continue <- liftIO $ sendSafely packet
|
||||||
|
when continue $
|
||||||
|
sendThread euphCon qRecv con
|
||||||
|
|
||||||
|
Just (SReply packetType packetData reply) -> do
|
||||||
|
(packet, packetID) <- preparePacket packetType packetData
|
||||||
|
void $ liftIO $ atomically $ writeTBQueue qRecv (RReply packetID reply)
|
||||||
|
continue <- liftIO $ sendSafely packet
|
||||||
|
when continue $
|
||||||
|
sendThread euphCon qRecv con
|
||||||
|
where
|
||||||
|
sendSafely packet = (WS.sendTextData con packet >> return True) `catch` handleException
|
||||||
|
handleException (WS.CloseRequest _ _) = return False
|
||||||
|
handleException WS.ConnectionClosed = return False
|
||||||
|
handleException _ = return True
|
||||||
|
|
||||||
|
{-
|
||||||
|
- Receive thread
|
||||||
|
-}
|
||||||
|
|
||||||
|
data PacketInfo = PacketInfo
|
||||||
|
{ infoPacketID :: Maybe PacketID
|
||||||
|
, infoServerError :: Maybe T.Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance FromJSON PacketInfo where
|
||||||
|
parseJSON = withObject "packet" $ \o -> do
|
||||||
|
infoPacketID <- o .:? "id"
|
||||||
|
infoServerError <- o .:? "error"
|
||||||
|
return PacketInfo{..}
|
||||||
|
|
||||||
|
-- TODO: Swap for HashMap?
|
||||||
|
type Awaiting = M.Map T.Text ReplyMVar
|
||||||
|
|
||||||
|
whenJust :: (Monad m) => Maybe a -> (a -> m ()) -> m ()
|
||||||
|
whenJust m f = maybe (return ()) f m
|
||||||
|
|
||||||
|
processPacket :: EventQueue -> BS.ByteString -> StateT Awaiting IO ()
|
||||||
|
processPacket qEvent bs = do
|
||||||
|
-- First, deal with event channel events.
|
||||||
|
case A.decode bs of
|
||||||
|
Nothing -> return ()
|
||||||
|
Just event -> liftIO $ atomically $ writeTBQueue qEvent (Just event)
|
||||||
|
-- Then, deal with replies.
|
||||||
|
-- Find out whether there is actually any dealing with replies to do...
|
||||||
|
replies <- get
|
||||||
|
let result = do -- Maybe monad
|
||||||
|
PacketInfo{..} <- A.decode bs
|
||||||
|
replyID <- infoPacketID
|
||||||
|
replyMVar <- M.lookup replyID replies
|
||||||
|
return (replyID, replyMVar, infoServerError)
|
||||||
|
-- ... and then write the appropriate result into the MVar.
|
||||||
|
whenJust result $ \(replyID, ReplyMVar var, serverError) -> do
|
||||||
|
modify (M.delete replyID)
|
||||||
|
case serverError of
|
||||||
|
Just e -> liftIO $ putMVar var (Left (EuphServerError e))
|
||||||
|
Nothing ->
|
||||||
|
case A.decode bs of
|
||||||
|
Nothing -> liftIO $ putMVar var (Left EuphParse)
|
||||||
|
Just r -> liftIO $ putMVar var (Right r)
|
||||||
|
|
||||||
|
processRecv :: RecvQueue -> EventQueue -> StateT Awaiting IO ()
|
||||||
|
processRecv qRecv qEvent = do
|
||||||
|
recv <- liftIO $ atomically $ readTBQueue qRecv
|
||||||
|
case recv of
|
||||||
|
RReply packetID replyMVar -> do
|
||||||
|
modify (M.insert packetID replyMVar)
|
||||||
|
processRecv qRecv qEvent
|
||||||
|
|
||||||
|
RPacket bs -> do
|
||||||
|
processPacket qEvent bs
|
||||||
|
processRecv qRecv qEvent
|
||||||
|
|
||||||
|
RDisconnected -> return ()
|
||||||
|
|
||||||
|
cleanupWaiting :: Awaiting -> IO ()
|
||||||
|
cleanupWaiting replies =
|
||||||
|
forM_ replies $ \(ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
||||||
|
|
||||||
|
emptyTBQueue :: TBQueue a -> STM [a]
|
||||||
|
emptyTBQueue q = do
|
||||||
|
isEmpty <- isEmptyTBQueue q
|
||||||
|
if isEmpty
|
||||||
|
then return []
|
||||||
|
else do
|
||||||
|
item <- readTBQueue q
|
||||||
|
rest <- emptyTBQueue q
|
||||||
|
return $ item : rest
|
||||||
|
|
||||||
|
cleanupSend :: SendQueue -> IO ()
|
||||||
|
cleanupSend qSend = do
|
||||||
|
sends <- atomically $ emptyTBQueue qSend
|
||||||
|
forM_ sends $ \case
|
||||||
|
SReply _ _ (ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
cleanupRecv :: RecvQueue -> IO ()
|
||||||
|
cleanupRecv qRecv = do
|
||||||
|
recvs <- atomically $ emptyTBQueue qRecv
|
||||||
|
forM_ recvs $ \case
|
||||||
|
RReply _ (ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
recvThread :: Connection -> RecvQueue -> WS.Connection -> IO ()
|
||||||
|
recvThread euphCon@(Connection locked qSend qEvent) qRecv con = do
|
||||||
|
tFetch <- async $ fetchThread qRecv con
|
||||||
|
tSend <- async $ evalStateT (sendThread euphCon qRecv con) 0
|
||||||
|
waitingReplies <- execStateT (processRecv qRecv qEvent) M.empty
|
||||||
|
atomically $ writeTVar locked True
|
||||||
|
wait tFetch
|
||||||
|
wait tSend
|
||||||
|
cleanupWaiting waitingReplies
|
||||||
|
cleanupSend qSend
|
||||||
|
cleanupRecv qRecv
|
||||||
|
atomically $ writeTBQueue qEvent Nothing
|
||||||
|
|
||||||
|
{-
|
||||||
|
- API functions
|
||||||
|
-}
|
||||||
|
|
||||||
|
sendPacket :: (ToJSON p, FromJSON r) => Connection -> T.Text -> p -> IO r
|
||||||
|
sendPacket (Connection locked qSend _) packetType packetData = do
|
||||||
|
var <- newEmptyMVar
|
||||||
|
let packet = SReply packetType packetData (ReplyMVar var)
|
||||||
|
atomically $ do
|
||||||
|
isLocked <- readTVar locked
|
||||||
|
if isLocked
|
||||||
|
then throwSTM EuphClosed
|
||||||
|
else writeTBQueue qSend packet
|
||||||
|
result <- readMVar var
|
||||||
|
case result of
|
||||||
|
Left f -> throw f
|
||||||
|
Right r -> return r
|
||||||
|
|
||||||
|
sendPacketNoReply :: (ToJSON p) => Connection -> T.Text -> p -> IO ()
|
||||||
|
sendPacketNoReply (Connection locked qSend _) packetType packetData = atomically $ do
|
||||||
|
let packet = SNoReply packetType packetData
|
||||||
|
isLocked <- readTVar locked
|
||||||
|
if isLocked
|
||||||
|
then throwSTM EuphClosed
|
||||||
|
else writeTBQueue qSend packet
|
||||||
|
|
||||||
|
pingReply :: Connection -> UTCTime -> IO ()
|
||||||
|
pingReply euphCon pingReplyCommandTime =
|
||||||
|
sendPacketNoReply euphCon "ping-reply" PingReplyCommand{..}
|
||||||
|
|
||||||
|
nick :: Connection -> T.Text -> IO (E.Nick, E.Nick)
|
||||||
|
nick euphCon nickCommandName = do
|
||||||
|
NickReply{..} <- sendPacket euphCon "nick" NickCommand{..}
|
||||||
|
return (nickReplyFrom, nickReplyTo)
|
||||||
|
|
||||||
|
send :: Connection -> Maybe E.Snowflake -> T.Text -> IO E.Message
|
||||||
|
send euphCon sendCommandParent sendCommandContent = do
|
||||||
|
SendReply{..} <- sendPacket euphCon "send" SendCommand{..}
|
||||||
|
return sendReplyMessage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{-
|
||||||
|
- Some types
|
||||||
|
-}
|
||||||
|
|
||||||
-- | The ways in which getting a reply from the server can fail.
|
-- | The ways in which getting a reply from the server can fail.
|
||||||
--
|
--
|
||||||
-- An EuphException may be raised by any function in the API functions section.
|
-- An EuphException may be raised by any function in the API functions section.
|
||||||
|
|
@ -121,117 +304,16 @@ instance Show EuphException where
|
||||||
|
|
||||||
instance Exception EuphException
|
instance Exception EuphException
|
||||||
|
|
||||||
sendPacket :: (ToJSON p, FromJSON r) => EuphConnection -> T.Text -> p -> IO r
|
data Recv = RDisconnected
|
||||||
sendPacket (EuphConnection chan _) packetType packetData = do
|
| RPacket BS.ByteString
|
||||||
var <- newEmptyMVar
|
| RReply PacketID ReplyMVar
|
||||||
let packet = SReply packetType packetData (ReplyMVar var)
|
|
||||||
done <- E.writeChan chan packet
|
|
||||||
case done of
|
|
||||||
Nothing -> throw EuphClosed
|
|
||||||
Just () -> do
|
|
||||||
result <- readMVar var
|
|
||||||
case result of
|
|
||||||
Left f -> throw f
|
|
||||||
Right r -> return r
|
|
||||||
|
|
||||||
sendPacketNoReply :: (ToJSON p) => EuphConnection -> T.Text -> p -> IO ()
|
data Send = SDisconnect
|
||||||
sendPacketNoReply (EuphConnection chan _) packetType packetData = do
|
| forall p . (ToJSON p) => SNoReply T.Text p -- packet type and contents
|
||||||
let packet = SNoReply packetType packetData
|
| forall p . (ToJSON p) => SReply T.Text p ReplyMVar
|
||||||
done <- E.writeChan chan packet
|
|
||||||
case done of
|
|
||||||
Nothing -> throw EuphClosed
|
|
||||||
Just () -> return ()
|
|
||||||
|
|
||||||
{-
|
data Event
|
||||||
- API functions
|
= BounceEvent (Maybe T.Text) (Maybe [T.Text])
|
||||||
-}
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
pingReply :: EuphConnection -> UTCTime -> IO ()
|
|
||||||
pingReply econ pingReplyCommandTime =
|
|
||||||
sendPacketNoReply econ "ping-reply" PingReplyCommand{..}
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
nick :: EuphConnection -> T.Text -> IO (E.Nick, E.Nick)
|
|
||||||
nick econ nickCommandName = do
|
|
||||||
NickReply{..} <- sendPacket econ "nick" NickCommand{..}
|
|
||||||
return (nickReplyFrom, nickReplyTo)
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
send :: EuphConnection -> Maybe E.Snowflake -> T.Text -> IO E.Message
|
|
||||||
send econ sendCommandParent sendCommandContent = do
|
|
||||||
SendReply{..} <- sendPacket econ "send" SendCommand{..}
|
|
||||||
return sendReplyMessage
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Commands and replies
|
|
||||||
-}
|
|
||||||
|
|
||||||
(.?=) :: (ToJSON v, KeyValue kv) => T.Text -> Maybe v -> [kv]
|
|
||||||
k .?= (Just v) = [k .= v]
|
|
||||||
_ .?= Nothing = []
|
|
||||||
|
|
||||||
-- ping reply/command/whatever
|
|
||||||
|
|
||||||
newtype PingReplyCommand = PingReplyCommand
|
|
||||||
{ pingReplyCommandTime :: UTCTime
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
instance ToJSON PingReplyCommand where
|
|
||||||
toJSON PingReplyCommand{..} =
|
|
||||||
object ["time" .= utcTimeToPOSIXSeconds pingReplyCommandTime]
|
|
||||||
|
|
||||||
-- nick command and reply
|
|
||||||
|
|
||||||
newtype NickCommand = NickCommand
|
|
||||||
{ nickCommandName :: T.Text
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
instance ToJSON NickCommand where
|
|
||||||
toJSON NickCommand{..} =
|
|
||||||
object ["name" .= nickCommandName]
|
|
||||||
|
|
||||||
data NickReply = NickReply
|
|
||||||
{ nickReplySessionID :: E.SessionID
|
|
||||||
, nickReplyUserID :: E.UserID
|
|
||||||
, nickReplyFrom :: T.Text
|
|
||||||
, nickReplyTo :: T.Text
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
instance FromJSON NickReply where
|
|
||||||
parseJSON = withObject "NickReply" $ \o -> do
|
|
||||||
nickReplySessionID <- o .: "session_id"
|
|
||||||
nickReplyUserID <- o .: "id"
|
|
||||||
nickReplyFrom <- o .: "from"
|
|
||||||
nickReplyTo <- o .: "to"
|
|
||||||
return NickReply{..}
|
|
||||||
|
|
||||||
-- send command and reply
|
|
||||||
|
|
||||||
data SendCommand = SendCommand
|
|
||||||
{ sendCommandContent :: T.Text
|
|
||||||
, sendCommandParent :: Maybe E.Snowflake
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
instance ToJSON SendCommand where
|
|
||||||
toJSON SendCommand{..} =
|
|
||||||
object $ ("content" .= sendCommandContent) : ("parent" .?= sendCommandParent)
|
|
||||||
|
|
||||||
newtype SendReply = SendReply
|
|
||||||
{ sendReplyMessage :: E.Message
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
instance FromJSON SendReply where
|
|
||||||
parseJSON v = SendReply <$> parseJSON v
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Events
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- | Represents <http://api.euphoria.io/#asynchronous-events>.
|
|
||||||
--
|
|
||||||
-- These events may be sent from the server to the client at any time.
|
|
||||||
data EuphEvent = BounceEvent (Maybe T.Text) (Maybe [T.Text])
|
|
||||||
-- ^ A 'BounceEvent' indicates that access to a room is denied.
|
-- ^ A 'BounceEvent' indicates that access to a room is denied.
|
||||||
--
|
--
|
||||||
-- @'BounceEvent' (Maybe reason) (Maybe [authOption])@
|
-- @'BounceEvent' (Maybe reason) (Maybe [authOption])@
|
||||||
|
|
@ -302,7 +384,7 @@ data EuphEvent = BounceEvent (Maybe T.Text) (Maybe [T.Text])
|
||||||
-- LogoutEvent -- not implemented
|
-- LogoutEvent -- not implemented
|
||||||
-- PMInitiateEvent -- not implemented
|
-- PMInitiateEvent -- not implemented
|
||||||
|
|
||||||
instance FromJSON EuphEvent where
|
instance FromJSON Event where
|
||||||
parseJSON = withObject "Event" $ \o -> do
|
parseJSON = withObject "Event" $ \o -> do
|
||||||
tp <- o .: "type"
|
tp <- o .: "type"
|
||||||
dt <- o .: "data"
|
dt <- o .: "data"
|
||||||
|
|
@ -339,182 +421,59 @@ instance FromJSON EuphEvent where
|
||||||
pSnapshotEvent = withObject "SnapshotEvent" $ \o ->
|
pSnapshotEvent = withObject "SnapshotEvent" $ \o ->
|
||||||
SnapshotEvent <$> o .: "version" <*> o .: "listing" <*> o .: "log" <*> o .:? "nick"
|
SnapshotEvent <$> o .: "version" <*> o .: "listing" <*> o .: "log" <*> o .:? "nick"
|
||||||
|
|
||||||
{-
|
(.?=) :: (ToJSON v, KeyValue kv) => T.Text -> Maybe v -> [kv]
|
||||||
- Channels
|
k .?= (Just v) = [k .= v]
|
||||||
-}
|
_ .?= Nothing = []
|
||||||
|
|
||||||
type RecvChan = E.CloseableChan Recv
|
-- ping reply/command/whatever
|
||||||
data Recv = RDisconnected
|
|
||||||
| RPacket BS.ByteString
|
|
||||||
| RReply PacketID ReplyMVar
|
|
||||||
|
|
||||||
type SendChan = E.CloseableChan Send
|
newtype PingReplyCommand = PingReplyCommand
|
||||||
data Send = SDisconnect
|
{ pingReplyCommandTime :: UTCTime
|
||||||
| forall p . (ToJSON p) => SNoReply T.Text p -- packet type and contents
|
|
||||||
| forall p . (ToJSON p) => SReply T.Text p ReplyMVar
|
|
||||||
|
|
||||||
type EventChan = Chan Event
|
|
||||||
type Event = Maybe EuphEvent
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Fetch thread
|
|
||||||
-}
|
|
||||||
|
|
||||||
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 (WS.CloseRequest _ _) = void $ E.writeChan cRecv RDisconnected
|
|
||||||
handleException WS.ConnectionClosed = void $ E.writeChan cRecv RDisconnected
|
|
||||||
handleException _ = fetchThread cRecv con
|
|
||||||
|
|
||||||
{-
|
|
||||||
- Send thread
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- Prepare a single packet for sending
|
|
||||||
preparePacket :: (ToJSON p) => T.Text -> p -> StateT Integer IO (BS.ByteString, PacketID)
|
|
||||||
preparePacket packetType packetData = do
|
|
||||||
packetNr <- get
|
|
||||||
put $ packetNr + 1
|
|
||||||
let packetID = T.pack $ show packetNr
|
|
||||||
bytestr = encode . Object . HM.fromList $
|
|
||||||
[ ("id", A.String packetID)
|
|
||||||
, ("type", A.String packetType)
|
|
||||||
, ("data", toJSON packetData)
|
|
||||||
]
|
|
||||||
return (bytestr, packetID)
|
|
||||||
|
|
||||||
sendThread :: SendChan -> RecvChan -> WS.Connection -> StateT Integer IO ()
|
|
||||||
sendThread cSend cRecv con = do
|
|
||||||
item <- liftIO $ E.readChan cSend
|
|
||||||
case item of
|
|
||||||
Nothing ->
|
|
||||||
return ()
|
|
||||||
|
|
||||||
Just SDisconnect ->
|
|
||||||
liftIO $ WS.sendClose con ("Bye." :: T.Text)
|
|
||||||
|
|
||||||
Just (SNoReply packetType packetData) -> do
|
|
||||||
(packet, _) <- preparePacket packetType packetData
|
|
||||||
liftIO $ WS.sendTextData con packet
|
|
||||||
continue <- liftIO $ sendSafely packet
|
|
||||||
when continue $
|
|
||||||
sendThread cSend cRecv con
|
|
||||||
|
|
||||||
Just (SReply packetType packetData reply) -> do
|
|
||||||
(packet, packetID) <- preparePacket packetType packetData
|
|
||||||
void $ liftIO $ E.writeChan cRecv (RReply packetID reply)
|
|
||||||
continue <- liftIO $ sendSafely packet
|
|
||||||
when continue $
|
|
||||||
sendThread cSend cRecv con
|
|
||||||
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
|
|
||||||
-}
|
|
||||||
|
|
||||||
data PacketInfo = PacketInfo
|
|
||||||
{ infoPacketID :: Maybe PacketID
|
|
||||||
, infoServerError :: Maybe T.Text
|
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance FromJSON PacketInfo where
|
instance ToJSON PingReplyCommand where
|
||||||
parseJSON = withObject "packet" $ \o -> do
|
toJSON PingReplyCommand{..} =
|
||||||
infoPacketID <- o .:? "id"
|
object ["time" .= utcTimeToPOSIXSeconds pingReplyCommandTime]
|
||||||
infoServerError <- o .:? "error"
|
|
||||||
return PacketInfo{..}
|
|
||||||
|
|
||||||
-- TODO: Swap for HashMap?
|
-- nick command and reply
|
||||||
type Awaiting = M.Map T.Text ReplyMVar
|
|
||||||
|
|
||||||
processRecv :: RecvChan -> EventChan -> Awaiting -> IO Awaiting
|
newtype NickCommand = NickCommand
|
||||||
processRecv cRecv cEvent replies = do
|
{ nickCommandName :: T.Text
|
||||||
recv <- E.readChan cRecv
|
} deriving (Show)
|
||||||
case recv of
|
|
||||||
Just (RReply packetID replyMVar) -> do
|
|
||||||
let newReplies = M.insert packetID replyMVar replies
|
|
||||||
processRecv cRecv cEvent newReplies
|
|
||||||
|
|
||||||
Just (RPacket bs) -> do
|
instance ToJSON NickCommand where
|
||||||
newReplies <- processPacket cEvent bs replies
|
toJSON NickCommand{..} =
|
||||||
processRecv cRecv cEvent newReplies
|
object ["name" .= nickCommandName]
|
||||||
|
|
||||||
_ -> return replies
|
data NickReply = NickReply
|
||||||
|
{ nickReplySessionID :: E.SessionID
|
||||||
|
, nickReplyUserID :: E.UserID
|
||||||
|
, nickReplyFrom :: T.Text
|
||||||
|
, nickReplyTo :: T.Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
processPacket :: EventChan -> BS.ByteString -> Awaiting -> IO Awaiting
|
instance FromJSON NickReply where
|
||||||
processPacket cEvent bs replies = do
|
parseJSON = withObject "NickReply" $ \o -> do
|
||||||
-- First, deal with event channel events.
|
nickReplySessionID <- o .: "session_id"
|
||||||
case A.decode bs of
|
nickReplyUserID <- o .: "id"
|
||||||
Nothing -> return ()
|
nickReplyFrom <- o .: "from"
|
||||||
Just event -> writeChan cEvent (Just event)
|
nickReplyTo <- o .: "to"
|
||||||
-- Then, deal with replies.
|
return NickReply{..}
|
||||||
fromMaybe (return replies) $ do
|
|
||||||
PacketInfo{..} <- A.decode bs
|
|
||||||
replyID <- infoPacketID
|
|
||||||
(ReplyMVar var) <- M.lookup replyID replies
|
|
||||||
let newReplies = M.delete replyID replies
|
|
||||||
case infoServerError of
|
|
||||||
Nothing -> do
|
|
||||||
reply <- A.decode bs
|
|
||||||
return $ newReplies <$ putMVar var (Right reply)
|
|
||||||
Just e ->
|
|
||||||
return $ newReplies <$ putMVar var (Left (EuphServerError e))
|
|
||||||
|
|
||||||
cleanupWaiting :: Awaiting -> IO ()
|
-- send command and reply
|
||||||
cleanupWaiting replies =
|
|
||||||
forM_ replies $ \(ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
|
||||||
|
|
||||||
cleanupSend :: SendChan -> IO ()
|
data SendCommand = SendCommand
|
||||||
cleanupSend cSend = do
|
{ sendCommandContent :: T.Text
|
||||||
sends <- E.emptyChan cSend
|
, sendCommandParent :: Maybe E.Snowflake
|
||||||
forM_ sends $ \case
|
} deriving (Show)
|
||||||
SReply _ _ (ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
|
||||||
_ -> return ()
|
|
||||||
|
|
||||||
cleanupRecv :: RecvChan -> IO ()
|
instance ToJSON SendCommand where
|
||||||
cleanupRecv cRecv = do
|
toJSON SendCommand{..} =
|
||||||
recvs <- E.emptyChan cRecv
|
object $ ("content" .= sendCommandContent) : ("parent" .?= sendCommandParent)
|
||||||
forM_ recvs $ \case
|
|
||||||
RReply _ (ReplyMVar var) -> putMVar var (Left EuphDisconnected)
|
|
||||||
_ -> return ()
|
|
||||||
|
|
||||||
recvThread :: SendChan -> RecvChan -> EventChan -> WS.Connection -> IO ()
|
newtype SendReply = SendReply
|
||||||
recvThread cSend cRecv cEvent con = do
|
{ sendReplyMessage :: E.Message
|
||||||
tFetch <- async $ fetchThread cRecv con
|
} deriving (Show)
|
||||||
tSend <- async $ evalStateT (sendThread cSend cRecv con) 0
|
|
||||||
waitingReplies <- processRecv cRecv cEvent M.empty
|
|
||||||
E.closeChan cSend
|
|
||||||
wait tFetch
|
|
||||||
wait tSend
|
|
||||||
cleanupWaiting waitingReplies
|
|
||||||
cleanupSend cSend
|
|
||||||
cleanupRecv cRecv
|
|
||||||
writeChan cEvent Nothing
|
|
||||||
|
|
||||||
{-
|
instance FromJSON SendReply where
|
||||||
- Startup
|
parseJSON v = SendReply <$> parseJSON v
|
||||||
-}
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
data EuphConnection = EuphConnection SendChan EventChan
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
getEvents :: EuphConnection -> IO [EuphEvent]
|
|
||||||
getEvents (EuphConnection _ cEvent) = do
|
|
||||||
events <- getChanContents cEvent
|
|
||||||
return $ catMaybes $ takeWhile isJust events
|
|
||||||
|
|
||||||
-- TODO: Add proper documentation
|
|
||||||
euphClient :: WS.ClientApp EuphConnection
|
|
||||||
euphClient con = do
|
|
||||||
sendChan <- E.newOpenChan
|
|
||||||
recvChan <- E.newOpenChan
|
|
||||||
eventChan <- newChan
|
|
||||||
void $ forkIO $ recvThread sendChan recvChan eventChan con
|
|
||||||
return $ EuphConnection sendChan eventChan
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue