reflex-libtelnet-0.2.0.0: src/Reflex/LibTelnet.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-|
Module : Reflex.LibTelnet
Description : Reflex wrapper around libtelnet
Copyright : (c) 2019, 2022 Jack Kelly
License : GPL-3.0-or-later
Maintainer : jack@jackkelly.name
Stability : experimental
Portability : non-portable
How to run a libtelnet state tracker off Reflex 'Event's:
1. Construct a 'TelnetConfig' by using 'newTelnetConfig' to get an
empty config, and then fill it out using record updates or lenses.
2. Start a state tracker by calling 'telnet' on your config.
3. Wire the events from the returned 'TelnetEvents' into the rest of
your application.
-}
module Reflex.LibTelnet
( telnet
-- * Input Events
, TelnetConfig(..)
, newTelnetConfig
-- * Output Events
, TelnetEvents(..)
) where
import Control.Exception (catch)
import Control.Monad.IO.Class (MonadIO(..))
import Data.ByteString (ByteString)
import qualified Data.Dependent.Map as DMap
import Data.Dependent.Sum ((==>))
import Data.Functor ((<&>))
import Data.GADT.Compare.TH (deriveGCompare, deriveGEq)
import Data.GADT.Show.TH (deriveGShow)
import GHC.Generics (Generic)
import Network.Telnet.LibTelnet (Telnet)
import qualified Network.Telnet.LibTelnet as Telnet
import Network.Telnet.LibTelnet.Iac (Iac)
import Network.Telnet.LibTelnet.Options (Option)
import Reflex
-- | A @'TelnetConfig' t@ contains all the input events for a telnet
-- state tracker. When passed to 'telnet', the network will call each
-- libtelnet function when its corresponding event fires. An "input
-- event" describes all events the state tracker listens to; it
-- doesn't care whether the events are coming from "above" or "below"
-- itself in the application stack. Data arriving on the socket
-- (@recv@) is an "input event" from "below"; data pasing through
-- libtelnet on its way out to the socket (@send@) is also an "input
-- event", but from "above".
--
-- You will almost certainly want to:
--
-- 1. use 'newTelnetConfig' to get an empty 'TelnetConfig';
-- 2. replace the @recv@ event with incoming socket data; and
-- 3. replace the @send@ event with outgoing data from your application.
--
-- @since 0.1.0.0
data TelnetConfig t = TelnetConfig
{ options :: [Telnet.OptionSpec]
-- ^ Passed to 'Telnet.telnetInit'
, flags :: [Telnet.Flag]
-- ^ Passed to 'Telnet.telnetInit'
, recv :: Event t ByteString
-- ^ 'Telnet.telnetRecv' - "I just received this data, please decode it"
, send :: Event t ByteString
-- ^ 'Telnet.telnetSend' - "I want to send this data out, please encode it"
, iac :: Event t Iac
-- ^ 'Telnet.telnetIac'
, negotiate :: Event t (Iac, Option)
-- ^ 'Telnet.telnetNegotiate'
, subnegotiation :: Event t (Option, ByteString)
-- ^ 'Telnet.telnetSubnegotiation'
, beginCompress2 :: Event t ()
-- ^ 'Telnet.telnetBeginCompress2'
, newEnvironSend :: Event t [(Telnet.Var, ByteString)]
-- ^ 'Telnet.telnetNewEnvironSend'
, newEnviron :: Event t ( Telnet.IsInfo
, [(Telnet.Var, ByteString, ByteString)]
)
-- ^ 'Telnet.telnetNewEnviron'
, tTypeSend :: Event t ()
-- ^ 'Telnet.telnetTTypeSend'
, tTypeIs :: Event t ByteString
-- ^ 'Telnet.telnetTTypeIs'
, sendZmp :: Event t [ByteString]
-- ^ 'Telnet.telnetSendZmp'
, sendMssp :: Event t [(ByteString, [ByteString])]
-- ^ 'Telnet.telnetSendMssp'
}
deriving Generic -- ^ @since 0.2.0.0
-- | No options set and all events are 'never'.
--
-- @since 0.2.0.0
newTelnetConfig :: Reflex t => TelnetConfig t
newTelnetConfig = TelnetConfig
{ options = []
, flags = []
, recv = never
, send = never
, iac = never
, negotiate = never
, subnegotiation = never
, beginCompress2 = never
, newEnvironSend = never
, newEnviron = never
, tTypeSend = never
, tTypeIs = never
, sendZmp = never
, sendMssp = never
}
-- | The libtelnet 'Telnet.Event' type is fanned out into a set of
-- individual "output events". An "output event" describes all events
-- that should be listened to, regardless of whether they are going
-- "up" or "down" the application stack: parsed data (@received@) is
-- an "output event" that should be listened to by the layer "above";
-- encoded data that should go to a socket (@send@) is also an
-- "output event", but listened to by the layer below.
--
-- @since 0.1.0.0
data TelnetEvents t = TelnetEvents
{ received :: Event t ByteString
-- ^ 'Telnet.Received' - "Here is some decoded data, please send
-- it up to the application"
, send :: Event t ByteString
-- ^ 'Telnet.Send' - "Here is some encoded data, please send it
-- out on the socket"
, warning :: Event t Telnet.Err
-- ^ 'Telnet.Warning'
, error :: Event t Telnet.Err
-- ^ 'Telnet.Error'
, iac :: Event t Iac
-- ^ 'Telnet.Iac'
, will :: Event t Option
-- ^ 'Telnet.Will'
, wont :: Event t Option
-- ^ 'Telnet.Wont'
, do_ :: Event t Option
-- ^ 'Telnet.Do'
, dont :: Event t Option
-- ^ 'Telnet.Dont'
, subnegotiation :: Event t (Option, ByteString)
-- ^ 'Telnet.Subnegotiation'
, zmp :: Event t [ByteString]
-- ^ 'Telnet.Zmp'
, terminalTypeSend :: Event t ()
-- ^ 'Telnet.TerminalTypeSend'
, terminalTypeIs :: Event t ByteString
-- ^ 'Telnet.TerminalTypeIs'
, compress :: Event t Bool
-- ^ 'Telnet.Compress'
, environSend :: Event t [(Telnet.Var, ByteString)]
-- ^ 'Telnet.EnvironSend'
, environ :: Event t (Telnet.IsInfo, [(Telnet.Var, ByteString, ByteString)])
-- ^ 'Telnet.Environ'
, mssp :: Event t [(ByteString, [ByteString])]
-- ^ 'Telnet.Mssp'
, exception :: Event t Telnet.TelnetException
-- ^ Exceptions thrown by the binding are caught and emitted
-- here. Protocol errors and warnings are emitted on the
-- @warning@ and @error@ events.
}
deriving Generic -- ^ @since 0.2.0.0
data EventKey a where
Received :: EventKey ByteString
Send :: EventKey ByteString
Warning :: EventKey Telnet.Err
Error :: EventKey Telnet.Err
Iac :: EventKey Iac
Will :: EventKey Option
Wont :: EventKey Option
Do :: EventKey Option
Dont :: EventKey Option
Subnegotiation :: EventKey (Option, ByteString)
Zmp :: EventKey [ByteString]
TerminalTypeSend :: EventKey ()
TerminalTypeIs :: EventKey ByteString
Compress :: EventKey Bool
EnvironSend :: EventKey [(Telnet.Var, ByteString)]
Environ :: EventKey (Telnet.IsInfo, [(Telnet.Var, ByteString, ByteString)])
Mssp :: EventKey [(ByteString, [ByteString])]
$(deriveGEq ''EventKey)
$(deriveGCompare ''EventKey)
$(deriveGShow ''EventKey)
-- | Process telnet streams using
-- <https://github.com/seanmiddleditch/libtelnet libtelnet>. Use
-- 'newTelnetConfig' to get a @'TelnetConfig' t@, and update it as
-- required.
--
-- @since 0.1.0.0
telnet
:: forall t m .
( MonadIO m, MonadIO (Performable m)
, PerformEvent t m
, TriggerEvent t m
)
=> TelnetConfig t
-> m (TelnetEvents t)
telnet config = do
(telnetE, fireTelnetE) <- newTriggerEvent
(telnetExcE, fireTelnetExcE) <- newTriggerEvent
t <- liftIO $ Telnet.telnetInit
(options config)
(flags config)
(const fireTelnetE)
let
perform
:: (Telnet -> a -> IO ())
-> (TelnetConfig t -> Event t a)
-> m ()
perform act get = performEvent_ $ action <$> get config where
action x = liftIO $ act t x `catch` \(ex :: Telnet.TelnetException) ->
fireTelnetExcE ex
perform Telnet.telnetRecv recv
perform Telnet.telnetSend send
perform Telnet.telnetIac iac
perform (uncurry . Telnet.telnetNegotiate) negotiate
perform (uncurry . Telnet.telnetSubnegotiation) subnegotiation
perform (const . Telnet.telnetBeginCompress2) beginCompress2
perform Telnet.telnetNewEnvironSend newEnvironSend
perform (uncurry . Telnet.telnetNewEnviron) newEnviron
perform (const . Telnet.telnetTTypeSend) tTypeSend
perform Telnet.telnetTTypeIs tTypeIs
perform Telnet.telnetSendZmp sendZmp
perform Telnet.telnetSendMssp sendMssp
let
selector :: EventSelector t EventKey
selector = fan $ telnetE <&> DMap.fromList . pure . \case
Telnet.Received b -> Received ==> b
Telnet.Send b -> Send ==> b
Telnet.Warning e -> Warning ==> e
Telnet.Error e -> Error ==> e
Telnet.Iac i -> Iac ==> i
Telnet.Will o -> Will ==> o
Telnet.Wont o -> Wont ==> o
Telnet.Do o -> Do ==> o
Telnet.Dont o -> Dont ==> o
Telnet.Subnegotiation o b -> Subnegotiation ==> (o, b)
Telnet.Zmp bs -> Zmp ==> bs
Telnet.TerminalTypeSend -> TerminalTypeSend ==> ()
Telnet.TerminalTypeIs term -> TerminalTypeIs ==> term
Telnet.Compress c -> Compress ==> c
Telnet.EnvironSend envs -> EnvironSend ==> envs
Telnet.Environ isInfo envs -> Environ ==> (isInfo, envs)
Telnet.Mssp msg -> Mssp ==> msg
pure $ TelnetEvents
{ received = selector `select` Received
, send = selector `select` Send
, warning = selector `select` Warning
, error = selector `select` Error
, iac = selector `select` Iac
, will = selector `select` Will
, wont = selector `select` Wont
, do_ = selector `select` Do
, dont = selector `select` Dont
, subnegotiation = selector `select` Subnegotiation
, zmp = selector `select` Zmp
, terminalTypeSend = selector `select` TerminalTypeSend
, terminalTypeIs = selector `select` TerminalTypeIs
, compress = selector `select` Compress
, environSend = selector `select` EnvironSend
, environ = selector `select` Environ
, mssp = selector `select` Mssp
, exception = telnetExcE
}