Connect to euphoria and run example bots

This commit is contained in:
Joscha 2020-01-06 17:48:36 +00:00
parent ee55f84c34
commit 3dbed10ffd
4 changed files with 300 additions and 114 deletions

View file

@ -20,6 +20,17 @@ description: Please see the README on GitHub at <https://github.com/Garm
dependencies: dependencies:
- base >= 4.7 && < 5 - base >= 4.7 && < 5
- aeson
- bytestring
- containers
- network
- stm
- text
- time
- transformers
- unordered-containers
- websockets
- wuss
library: library:
source-dirs: src source-dirs: src

View file

@ -1,9 +1,9 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Haboli.Euphoria.Api module Haboli.Euphoria.Api
( ( ToJSONObject(..)
-- * Basic types -- * Basic types
AuthOption(..) , AuthOption(..)
, Message(..) , Message(..)
, PersonalAccountView , PersonalAccountView
, SessionView(..) , SessionView(..)
@ -11,35 +11,59 @@ module Haboli.Euphoria.Api
, UserType(..) , UserType(..)
, UserId(..) , UserId(..)
-- * Asynchronous events -- * Asynchronous events
, BounceEvent , BounceEvent(..)
, DisconnectEvent , DisconnectEvent(..)
, HelloEvent , HelloEvent(..)
, JoinEvent , JoinEvent(..)
, LoginEvent , LoginEvent(..)
, LogoutEvent , LogoutEvent(..)
, NetworkEvent , NetworkEvent(..)
, NickEvent , NickEvent(..)
, EditMessageEvent , EditMessageEvent(..)
, PartEvent , PartEvent(..)
, PingEvent , PingEvent(..)
, PmInitiateEvent , PmInitiateEvent(..)
, SendEvent , SendEvent(..)
, SnapshotEvent , SnapshotEvent(..)
-- * Session commands -- * Session commands
-- ** auth -- ** auth
, AuthCommand , AuthCommand(..)
, AuthReply , AuthReply(..)
-- ** ping -- ** ping
, PingCommand , PingCommand(..)
, PingReply , PingReply(..)
-- * Chat room commands
-- ** nick
, NickCommand(..)
, NickReply(..)
) where ) where
import Control.Monad
import Data.Aeson import Data.Aeson
import Data.Aeson.Types import Data.Aeson.Types
import qualified Data.HashMap.Strict as HMap
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time import Data.Time
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
class ToJSONObject a where
toJSONObject :: a -> Object
fromPacket :: T.Text -> (Object -> Parser a) -> Value -> Parser a
fromPacket packetType parser (Object o) = do
actualType <- o .: "type"
when (actualType /= packetType) $
fail $ T.unpack $ "packet type is not " <> packetType
packetData <- o .: "data"
parser packetData
fromPacket _ _ v = typeMismatch "Object" v
toPacket :: T.Text -> Value -> Object
toPacket packetType packetData = HMap.fromList
[ "type" .= packetType
, "data" .= packetData
]
{- Basic types -} {- Basic types -}
data AuthOption = Passcode data AuthOption = Passcode
@ -50,8 +74,8 @@ instance ToJSON AuthOption where
instance FromJSON AuthOption where instance FromJSON AuthOption where
parseJSON (String "passcode") = pure Passcode parseJSON (String "passcode") = pure Passcode
parseJSON (String _) = fail "invalid value" parseJSON (String _) = fail "invalid value"
parseJSON v = typeMismatch "String" v parseJSON v = typeMismatch "String" v
-- | A 'Message' is a node in a rooms log. It corresponds to a chat message, or -- | 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. See -- a post, or any broadcasted event in a room that should appear in the log. See
@ -211,6 +235,11 @@ data PingEvent = PingEvent
, pingNext :: UTCTime , pingNext :: UTCTime
} deriving (Show) } deriving (Show)
instance FromJSON PingEvent where
parseJSON = fromPacket "ping-event" $ \o -> PingEvent
<$> (posixSecondsToUTCTime <$> o .: "time")
<*> (posixSecondsToUTCTime <$> o .: "next")
data PmInitiateEvent = PmInitiateEvent data PmInitiateEvent = PmInitiateEvent
{ pmInitiateFrom :: UserId { pmInitiateFrom :: UserId
, pmInitiateFromNick :: T.Text , pmInitiateFromNick :: T.Text
@ -222,16 +251,30 @@ data SendEvent = SendEvent
{ sendMessage :: Message { sendMessage :: Message
} deriving (Show) } deriving (Show)
{- snapshot-event -}
data SnapshotEvent = SnapshotEvent data SnapshotEvent = SnapshotEvent
{ snapshotIdentity :: UserId { snapshotIdentity :: UserId
, snapshotSessionId :: T.Text
, snapshotVersion :: T.Text , snapshotVersion :: T.Text
, snapshotListing :: [SessionView] , snapshotListing :: [SessionView]
, snapshotLog :: [Message] , snapshotLog :: [Message]
, snapshotNick :: Maybe T.Text , snapshotNick :: Maybe T.Text
, snapshotPmWithNick :: T.Text , snapshotPmWithNick :: Maybe T.Text
, snapshotPmWithUserId :: UserId , snapshotPmWithUserId :: Maybe UserId
} deriving (Show) } deriving (Show)
instance FromJSON SnapshotEvent where
parseJSON = fromPacket "snapshot-event" $ \o -> SnapshotEvent
<$> o .: "identity"
<*> o .: "session_id"
<*> o .: "version"
<*> o .: "listing"
<*> o .: "log"
<*> o .:? "nick"
<*> o .:? "pm_with_nick"
<*> o .:? "pm_with_user_id"
{- Session commands -} {- Session commands -}
{- auth -} {- auth -}
@ -247,9 +290,23 @@ data AuthReply = AuthSuccessful | AuthFailed T.Text
data PingCommand = PingCommand UTCTime data PingCommand = PingCommand UTCTime
deriving (Show) deriving (Show)
instance ToJSONObject PingCommand where
toJSONObject (PingCommand time) = toPacket "ping-reply" $ object
[ "time" .= utcTimeToPOSIXSeconds time
]
data PingReply = PingReply UTCTime data PingReply = PingReply UTCTime
deriving (Show) deriving (Show)
instance ToJSONObject PingReply where
toJSONObject (PingReply time) = toPacket "ping-reply" $ object
[ "time" .= utcTimeToPOSIXSeconds time
]
instance FromJSON PingReply where
parseJSON = fromPacket "ping-reply" $ \o -> PingReply
<$> (posixSecondsToUTCTime <$> o .: "time")
{- Chat room commands -} {- Chat room commands -}
{- nick -} {- nick -}
@ -257,23 +314,22 @@ data PingReply = PingReply UTCTime
data NickCommand = NickCommand T.Text data NickCommand = NickCommand T.Text
deriving (Show) deriving (Show)
instance ToJSON NickCommand where instance ToJSONObject NickCommand where
toJSON (NickCommand nick) = object toJSONObject (NickCommand nick) = HMap.fromList
[ "type" .= String "nick" [ "type" .= String "nick"
, "data" .= object ["name" .= nick] , "data" .= object ["name" .= nick]
] ]
data NickReply = NickReply data NickReply = NickReply
{ nickReplySessionId :: T.Text { nickReplySessionId :: T.Text
, nickReplyId :: UserId , nickReplyId :: UserId
, nickReplyFrom :: T.Text , nickReplyFrom :: T.Text
, nickReplyTo :: T.Text , nickReplyTo :: T.Text
} deriving (Show) } deriving (Show)
instance FromJSON NickReply where instance FromJSON NickReply where
parseJSON (Object o) = NickReply parseJSON = fromPacket "nick-reply" $ \o -> NickReply
<$> o .: "session_id" <$> o .: "session_id"
<*> o .: "id" <*> o .: "id"
<*> o .: "from" <*> o .: "from"
<*> o .: "to" <*> o .: "to"
parseJSON v = typeMismatch "Object" v

View file

@ -1,27 +1,49 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
module Haboli.Euphoria.Client where module Haboli.Euphoria.Client
(
-- * The Client monad
Client
, runClient
, ConnectionDetails(..)
, defaultDetails
-- ** Getters
, getHost
, getPort
, getPath
-- ** Event handling
, Event(..)
, nextEvent
, respondingToPing
-- ** Exception handling
, ClientException(..)
, Haboli.Euphoria.Client.throw
-- ** Euphoria commands
-- *** Session commands
, pingReply
-- *** Chat room commands
, nick
) where
import Control.Exception import Control.Applicative
import Control.Concurrent import Control.Concurrent
import Control.Concurrent.Chan import Control.Concurrent.STM
import Data.Foldable import Control.Exception
import Control.Monad.Trans.Class
import Data.Traversable
import Control.Concurrent.MVar
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader
import Control.Concurrent.STM
import Control.Concurrent.STM.TVar
import Control.Concurrent.STM.TMVar
import Data.Aeson import Data.Aeson
import Data.Aeson.Types
import qualified Data.ByteString.Lazy as BS
import Data.Foldable
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time import Data.Time
import Network.Socket import Network.Socket
import qualified Network.WebSockets as WS import qualified Network.WebSockets as WS
@ -30,23 +52,18 @@ import qualified Wuss as WSS
import Haboli.Euphoria.Api import Haboli.Euphoria.Api
--TODO: Add all the events --TODO: Add all the events
-- | An event sent by the server. See
-- <http://api.euphoria.io/#asynchronous-events>.
data ServerEvent
= ServerHello HelloEvent
| ServerSnapshot SnapshotEvent
deriving (Show)
-- | An event coming from the connection to the server.
data Event data Event
= EventServer ServerEvent = EventPing PingEvent
-- ^ The server has sent an event. | EventSnapshot SnapshotEvent
| EventStopped | PlaceholderEvent --TODO: remove this event
-- ^ The connection has been closed. This event is always the last event and
-- after this event, no other event will come from the connection.
deriving (Show) deriving (Show)
--TODO: Decide between Exception and Error instance FromJSON Event where
parseJSON v = foldr (<|>) mempty
[ EventPing <$> parseJSON v
, EventSnapshot <$> parseJSON v
]
--TODO: Add more exceptions for other things that can also go wrong (parsing, connection already closed, ...) --TODO: Add more exceptions for other things that can also go wrong (parsing, connection already closed, ...)
data ClientException e data ClientException e
= ServerException (Maybe T.Text) (Maybe T.Text) = ServerException (Maybe T.Text) (Maybe T.Text)
@ -57,31 +74,41 @@ data ClientException e
| StoppedException | StoppedException
| DecodeException T.Text | DecodeException T.Text
-- ^ At some point during decoding a websocket packet, something went wrong. -- ^ At some point during decoding a websocket packet, something went wrong.
| CustomError e | CustomException e
deriving (Show) deriving (Show)
instance FromJSON (ClientException e) where
parseJSON (Object o) = do
serverError <- o .:? "error"
isThrottled <- o .:? "throttled" .!= False
throttledReason <- o .:? "throttled_reason"
let throttled = if isThrottled then Just (fromMaybe "" throttledReason) else Nothing
when (isNothing serverError && isNothing throttled) $
fail "there is no error and the client is not throttled"
pure $ ServerException serverError throttled
parseJSON v = typeMismatch "Object" v
-- | This type is used by the websocket thread to send the server's replies to -- | This type is used by the websocket thread to send the server's replies to
-- the client. Since exceptions like a 'ServerError' may occur, they are -- the client. Since exceptions like a 'ServerError' may occur, they are
-- explicitly included in the type stored in the 'MVar'. -- explicitly included in the type stored in the 'MVar'.
-- --
-- The fancy types are there so I don't have to explicitly specify the response -- The fancy types are there so I don't have to explicitly specify the response
-- in some sum type or similar. -- in some sum type or similar.
newtype AwaitingReply e data AwaitingReply e
= AwaitingReply (forall r. FromJSON r => Either (ClientException e) r) = forall r. FromJSON r => AwaitingReply (TMVar (Either (ClientException e) r))
-- | A 'Map.Map' of empty 'TMVar's waiting for their respective reply packet -- | A 'Map.Map' of empty 'TMVar's waiting for their respective reply packet
-- from the server. -- from the server.
type AwaitingReplies e = Map.Map T.Text (TMVar (AwaitingReply e)) type AwaitingReplies e = Map.Map T.Text (AwaitingReply e)
data ClientInfo e = ClientInfo data ClientInfo e = ClientInfo
{ ciDetails :: ConnectionDetails { ciDetails :: ConnectionDetails
, ciConnection :: WS.Connection , ciConnection :: WS.Connection
, ciPacketId :: TVar Integer , ciAwaiting :: TVar (AwaitingReplies e)
, ciWsThreadId :: ThreadId , ciEventChan :: TChan Event
, ciAwaiting :: TVar (AwaitingReplies e) , ciPacketId :: TVar Integer
, ciEventChan :: Chan Event , ciStopped :: TVar Bool -- only modified by websocket thread
, ciStopped :: TVar Bool -- only modified by websocket thread }
}
-- This type declaration feels lispy in its parenthesisness -- This type declaration feels lispy in its parenthesisness
newtype Client e a = Client (ReaderT (ClientInfo e) newtype Client e a = Client (ReaderT (ClientInfo e)
@ -91,35 +118,56 @@ newtype Client e a = Client (ReaderT (ClientInfo e)
{- The websocket listening thread -} {- The websocket listening thread -}
--TODO: This could close the ws connection and stop the client instead
-- | An exception handler that ignores messages that could not be decoded
-- properly. It only prints the exceptions via 'putStrLn'.
ignoringInvalidMessages :: WS.ConnectionException -> IO () ignoringInvalidMessages :: WS.ConnectionException -> IO ()
ignoringInvalidMessages (WS.ParseException message) = putStrLn $ "ParseException: " ++ message ignoringInvalidMessages (WS.ParseException message) = putStrLn $ "ParseException: " ++ message
ignoringInvalidMessages (WS.UnicodeException message) = putStrLn $ "UnicodeException: " ++ message ignoringInvalidMessages (WS.UnicodeException message) = putStrLn $ "UnicodeException: " ++ message
ignoringInvalidMessages e = throwIO e ignoringInvalidMessages e = throwIO e
cancelAllReplies :: TVar (AwaitingReplies e) -> STM () -- | An exception handler that stops the client if any sort of
cancelAllReplies awaiting = do -- 'WS.ConnectionException' occurs. It does this by setting 'ciStopped' to True
replyMap <- readTVar awaiting -- and cancelling all 'AwaitingReply'-s in 'ciAwaiting'.
for_ replyMap $ \v -> cancellingAllReplies :: ClientInfo e -> WS.ConnectionException -> IO ()
putTMVar v (AwaitingReply (Left StoppedException)) cancellingAllReplies info _ = atomically $ do
writeTVar (ciStopped info) True
-- Cancel all replies
replyMap <- readTVar (ciAwaiting info)
for_ replyMap $ \(AwaitingReply v) ->
putTMVar v (Left StoppedException)
wsThread :: WS.Connection -> Chan Event -> TVar (AwaitingReplies e) -> TVar Bool -> IO () parseAndSendEvent :: BS.ByteString -> TChan Event -> IO ()
wsThread connection eventChan awaiting stopped parseAndSendEvent msg eventChan =
= handle stopHandler for_ (decode msg) $ \event ->
$ forever atomically $ writeTChan eventChan event
$ handle ignoringInvalidMessages
$ do parseAndSendReply :: BS.ByteString -> TVar (AwaitingReplies e) -> IO ()
msg <- WS.receiveData connection parseAndSendReply msg awaiting = do
--TODO: Actually parse the stuff and send it to the event channel let maybePacketId = parseMaybe parsePacketId =<< decode msg
T.putStrLn msg for_ maybePacketId $ \packetId -> atomically $ do
awaitingMap <- readTVar awaiting
for_ (awaitingMap Map.!? packetId) $ \(AwaitingReply replyVar) -> do
let maybeExceptionOrReply = (Left <$> decode msg) <|> (Right <$> decode msg)
invalidStructureException = Left $ DecodeException "invalid message json structure"
putTMVar replyVar $ fromMaybe invalidStructureException maybeExceptionOrReply
modifyTVar awaiting $ Map.delete packetId
where where
stopHandler :: WS.ConnectionException -> IO () parsePacketId :: Value -> Parser T.Text
stopHandler _ = do parsePacketId (Object o) = o .: "id"
-- After 'stopped' is set to True, 'awaiting' is not modified by any parsePacketId v = typeMismatch "Object" v
-- thread. Because of this, the call to 'cancelAllReplies' wouldn't need
-- to happen atomically with setting 'stopped' to True, but I still do it runWebsocketThread :: ClientInfo e -> IO ()
-- atomically. runWebsocketThread info
atomically $ writeTVar stopped True >> cancelAllReplies awaiting = WS.withPingThread (ciConnection info) pingInterval (pure ())
writeChan eventChan EventStopped $ handle (cancellingAllReplies info) $ forever
$ handle ignoringInvalidMessages $ do
msg <- WS.receiveData (ciConnection info)
-- print msg
parseAndSendEvent msg (ciEventChan info)
parseAndSendReply msg (ciAwaiting info)
where
pingInterval = cdPingInterval $ ciDetails info
{- Running the Client monad -} {- Running the Client monad -}
@ -138,30 +186,37 @@ defaultDetails = ConnectionDetails
, cdPingInterval = 10 , cdPingInterval = 10
} }
--TODO: Close connection after client finishes running if it hasn't already been closed
runClient :: ConnectionDetails -> Client e a -> IO (Either (ClientException e) a) runClient :: ConnectionDetails -> Client e a -> IO (Either (ClientException e) a)
runClient details (Client stack) runClient details (Client stack)
= withSocketsDo = withSocketsDo
$ WSS.runSecureClient (cdHost details) (cdPort details) (cdPath details) $ WSS.runSecureClient (cdHost details) (cdPort details) (cdPath details)
$ \connection -> do $ \connection -> do
packetId <- newTVarIO 0
awaiting <- newTVarIO Map.empty awaiting <- newTVarIO Map.empty
eventChan <- newChan eventChan <- newTChanIO
packetId <- newTVarIO 0
stopped <- newTVarIO False stopped <- newTVarIO False
wsThreadId <- forkIO
$ WS.withPingThread connection (cdPingInterval details) (pure ())
$ wsThread connection eventChan awaiting stopped
let info = ClientInfo let info = ClientInfo
{ ciDetails = details { ciDetails = details
, ciConnection = connection , ciConnection = connection
, ciPacketId = packetId
, ciWsThreadId = wsThreadId
, ciAwaiting = awaiting , ciAwaiting = awaiting
, ciEventChan = eventChan , ciEventChan = eventChan
, ciPacketId = packetId
, ciStopped = stopped , ciStopped = stopped
} }
runExceptT $ runReaderT stack info -- Start the websocket thread, which will notify this thread when it stops
wsThreadFinished <- newEmptyMVar
void $ forkFinally (runWebsocketThread info) (\_ -> putMVar wsThreadFinished ())
-- Run the actual 'Client' in this thread
result <- runExceptT $ runReaderT stack info
-- Close the connection if it is not already closed, and wait until the
-- websocket thread stops
handle ignoreAllExceptions $ WS.sendClose connection $ T.pack "Goodbye :D"
takeMVar wsThreadFinished
pure result
where
ignoreAllExceptions :: WS.ConnectionException -> IO ()
ignoreAllExceptions _ = pure ()
{- Private operations -} {- Private operations -}
@ -199,42 +254,42 @@ safeSend connection packet = do
$ "could not decode unicode: " <> T.pack message $ "could not decode unicode: " <> T.pack message
-- | Send a packet and automatically add a packet id -- | Send a packet and automatically add a packet id
sendPacket :: Object -> Client e T.Text sendPacket :: ToJSONObject o => o -> Client e T.Text
sendPacket packet = do sendPacket packet = do
connection <- ciConnection <$> getClientInfo connection <- ciConnection <$> getClientInfo
-- No need to check if 'ciStopped' is True because 'WS.sendTextData' will -- No need to check if 'ciStopped' is True because 'WS.sendTextData' will
-- throw an exception anyways. -- throw an exception anyways.
packetId <- newPacketId packetId <- newPacketId
let packetWithId = packet <> ("id" .= packetId) let packetWithId = toJSONObject packet <> ("id" .= packetId)
safeSend connection packetWithId safeSend connection packetWithId
pure packetId pure packetId
-- | Send a packet and wait for a reply from the server. -- | Send a packet and wait for a reply from the server.
sendPacketWithReply :: FromJSON r => Object -> Client e r sendPacketWithReply :: (ToJSONObject o, FromJSON r) => o -> Client e r
sendPacketWithReply packet = do sendPacketWithReply packet = do
info <- getClientInfo info <- getClientInfo
packetId <- sendPacket packet packetId <- sendPacket packet
-- Create and insert a new empty TMVar into the AwaitingReplies map
maybeReplyVar <- liftIO $ atomically $ do maybeReplyVar <- liftIO $ atomically $ do
stopped <- readTVar $ ciStopped info stopped <- readTVar $ ciStopped info
if stopped if stopped
then pure Nothing then pure Nothing
else do else do
replyVar <- newEmptyTMVar replyVar <- newEmptyTMVar
modifyTVar (ciAwaiting info) (Map.insert packetId replyVar) modifyTVar (ciAwaiting info) $ Map.insert packetId (AwaitingReply replyVar)
pure $ Just replyVar pure $ Just replyVar
case maybeReplyVar of case maybeReplyVar of
Nothing -> throwRaw StoppedException Nothing -> throwRaw StoppedException
Just replyVar -> do Just replyVar -> do
(AwaitingReply reply) <- liftIO $ atomically $ do reply <- liftIO $ atomically $ readTMVar replyVar
reply <- readTMVar replyVar
modifyTVar (ciAwaiting info) (Map.delete packetId)
pure reply
case reply of case reply of
Left e -> throwRaw e Left e -> throwRaw e
Right r -> pure r Right r -> pure r
{- Public operations -} {- Public operations -}
{- Getters -}
getHost :: Client e HostName getHost :: Client e HostName
getHost = cdHost . ciDetails <$> getClientInfo getHost = cdHost . ciDetails <$> getClientInfo
@ -244,7 +299,45 @@ getPort = cdPort . ciDetails <$> getClientInfo
getPath :: Client e String getPath :: Client e String
getPath = cdPath . ciDetails <$> getClientInfo getPath = cdPath . ciDetails <$> getClientInfo
stop :: Client e () {- Special operations -}
stop = do
ci <- getClientInfo nextEvent :: Client e Event
liftIO $ WS.sendClose (ciConnection ci) $ T.pack "Goodbye :D" nextEvent = do
info <- getClientInfo
exceptionOrEvent <- liftIO $ atomically $ do
stopped <- readTVar (ciStopped info)
if stopped
then pure $ Left StoppedException
else Right <$> readTChan (ciEventChan info)
case exceptionOrEvent of
Left e -> throwRaw e
Right e -> pure e
respondingToPing :: Client e Event -> Client e Event
respondingToPing holdingEvent = do
event <- holdingEvent
case event of
EventPing e -> pingReply (pingTime e)
_ -> pure ()
pure event
{- Exception handling -}
--TODO: Add more commands and find more appropriate names
throw :: e -> Client e a
throw = throwRaw . CustomException
{- Euphoria commands -}
{- Session commands -}
pingReply :: UTCTime -> Client e ()
pingReply = void . sendPacket . PingReply
{- Chat room commands -}
nick :: T.Text -> Client e T.Text
nick targetNick = do
reply <- sendPacketWithReply $ NickCommand targetNick
pure $ nickReplyTo reply

View file

@ -0,0 +1,26 @@
{-# LANGUAGE OverloadedStrings #-}
-- | This module contains a few basic example bots.
module Haboli.Euphoria.Example where
import Control.Monad
import Control.Monad.IO.Class
import Haboli.Euphoria.Client
printAllEventsBot :: Client () ()
printAllEventsBot = forever $ do
liftIO $ putStrLn "Waiting for the next event"
liftIO . print =<< respondingToPing nextEvent
setNickAndThenWaitBot :: Client () ()
setNickAndThenWaitBot = forever $ do
event <- respondingToPing nextEvent
case event of
EventSnapshot _ -> void $ nick "HaboliTestBot"
_ -> pure ()
throwCustomExceptionBot :: Client String ()
throwCustomExceptionBot = throw "Hello world"
immediatelyDisconnectBot :: Client () ()
immediatelyDisconnectBot = pure ()