first commit
This commit is contained in:
commit
a60065d447
37
krobotkin.cabal
Normal file
37
krobotkin.cabal
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
cabal-version: 1.12
|
||||||
|
|
||||||
|
name: krobotkin
|
||||||
|
version: 0.0.0.1
|
||||||
|
license: MIT
|
||||||
|
build-type: Simple
|
||||||
|
|
||||||
|
synopsis: xmpp muc bot
|
||||||
|
description: xmpp bot for the cyberia muc
|
||||||
|
|
||||||
|
author: Thorn Avery
|
||||||
|
maintainer: ta@p7.co.nz
|
||||||
|
|
||||||
|
executable krobotkin
|
||||||
|
main-is: Main.hs
|
||||||
|
ghc-options: -O2 -Wall
|
||||||
|
hs-source-dirs:
|
||||||
|
src
|
||||||
|
build-depends:
|
||||||
|
base
|
||||||
|
, pontarius-xmpp
|
||||||
|
, hslogger
|
||||||
|
, data-default
|
||||||
|
, xml-types
|
||||||
|
, text
|
||||||
|
, optparse-applicative
|
||||||
|
, random
|
||||||
|
, mtl
|
||||||
|
other-modules:
|
||||||
|
Krobotkin.Handlers
|
||||||
|
, Krobotkin.Monad
|
||||||
|
, Krobotkin.Muc
|
||||||
|
, Krobotkin.Options
|
||||||
|
, Krobotkin.Setup
|
||||||
|
, Krobotkin.Stanza
|
||||||
|
, Krobotkin.Types
|
||||||
|
default-language: Haskell2010
|
88
src/Krobotkin/Handlers.hs
Normal file
88
src/Krobotkin/Handlers.hs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Krobotkin.Handlers where
|
||||||
|
|
||||||
|
import Krobotkin.Monad
|
||||||
|
import Krobotkin.Muc
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Data.Maybe
|
||||||
|
import System.Random
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.State
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
dispatchStanza :: PackedStanza -> BotMonad ()
|
||||||
|
dispatchStanza (BotPresence fromUser ptype o) =
|
||||||
|
handlePresence fromUser ptype o
|
||||||
|
dispatchStanza (BotMessage toUser fromUser content o) =
|
||||||
|
handleMessage toUser fromUser content o
|
||||||
|
dispatchStanza _ = liftIO $ return ()
|
||||||
|
|
||||||
|
handlePresence :: Jid -> PresenceType -> Presence -> BotMonad ()
|
||||||
|
handlePresence fromUser Subscribe _ = approveSubscription fromUser
|
||||||
|
handlePresence _ _ _ = liftIO $ return ()
|
||||||
|
|
||||||
|
approveSubscription :: Jid -> BotMonad ()
|
||||||
|
approveSubscription user = do
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
_ <- liftIO $ sendPresence (presenceSubscribed user) sess
|
||||||
|
liftIO $ putStrLn $ "subscribed to " ++ (show user)
|
||||||
|
|
||||||
|
handleMessage :: Jid -> Jid -> String -> Message -> BotMonad ()
|
||||||
|
handleMessage _ fromUser "PING" _ =
|
||||||
|
sendMucMessage
|
||||||
|
"pong"
|
||||||
|
(toBare fromUser)
|
||||||
|
handleMessage _ fromUser "BING" _ =
|
||||||
|
sendMucMessage
|
||||||
|
"cannabis smoking device"
|
||||||
|
(toBare fromUser)
|
||||||
|
handleMessage _ fromUser "DING" _ =
|
||||||
|
sendMucMessage
|
||||||
|
"hehe penis 8==D~"
|
||||||
|
(toBare fromUser)
|
||||||
|
handleMessage _ fromUser ('.':'8':'B':'A':'L':'L':' ':_) o = do
|
||||||
|
rng <- gets stateRNG
|
||||||
|
let (x,rng') = randomR (0,(length responses)-1) rng
|
||||||
|
modify (\s -> s { stateRNG = rng' })
|
||||||
|
sendMucMessage
|
||||||
|
(T.pack ("> "
|
||||||
|
++ (drop 7 (T.unpack (fromMaybe ";0;" (getMucMessageBody o))))
|
||||||
|
++ "\n" ++ responses !! x))
|
||||||
|
(toBare fromUser)
|
||||||
|
where
|
||||||
|
responses =
|
||||||
|
[ "it is certain"
|
||||||
|
, "without a doubt"
|
||||||
|
, "you may rely on it"
|
||||||
|
, "yes definitely"
|
||||||
|
, "it is decidedly so"
|
||||||
|
, "as i see it, yes"
|
||||||
|
, "most likely"
|
||||||
|
, "yes"
|
||||||
|
, "outlook good"
|
||||||
|
, "signs point to yes"
|
||||||
|
, "reply hazy try again"
|
||||||
|
, "better not tell you now"
|
||||||
|
, "ask again later"
|
||||||
|
, "cannot predict now"
|
||||||
|
, "concentrate and ask again"
|
||||||
|
, "dont count on it"
|
||||||
|
, "outlook not so good"
|
||||||
|
, "my sources say no"
|
||||||
|
, "very doubtful"
|
||||||
|
, "my reply is no"
|
||||||
|
]
|
||||||
|
handleMessage _ fromUser _ _ | T.toUpper (fromMaybe "no" (resourcepart fromUser)) == "ELFIE" = do
|
||||||
|
rng <- gets stateRNG
|
||||||
|
let (x,rng') = randomR (0,21) rng :: (Int,StdGen)
|
||||||
|
modify (\s -> s { stateRNG = rng' })
|
||||||
|
if x == 13
|
||||||
|
then sendMucMessage "shut up smellfie" (toBare fromUser)
|
||||||
|
else liftIO $ return ()
|
||||||
|
handleMessage _ _ _ _ = liftIO $ return ()
|
84
src/Krobotkin/Handlers.hs~
Normal file
84
src/Krobotkin/Handlers.hs~
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Krobotkin.Handlers where
|
||||||
|
|
||||||
|
import Krobotkin.Monad
|
||||||
|
import Krobotkin.Muc
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Data.Maybe
|
||||||
|
import System.Random
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.State
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
dispatchStanza :: PackedStanza -> BotMonad ()
|
||||||
|
dispatchStanza (BotPresence fromUser ptype o) =
|
||||||
|
handlePresence fromUser ptype o
|
||||||
|
dispatchStanza (BotMessage toUser fromUser content o) =
|
||||||
|
handleMessage toUser fromUser content o
|
||||||
|
dispatchStanza _ = liftIO $ return ()
|
||||||
|
|
||||||
|
handlePresence :: Jid -> PresenceType -> Presence -> BotMonad ()
|
||||||
|
handlePresence fromUser Subscribe _ = approveSubscription fromUser
|
||||||
|
handlePresence _ _ _ = liftIO $ return ()
|
||||||
|
|
||||||
|
approveSubscription :: Jid -> BotMonad ()
|
||||||
|
approveSubscription user = do
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
_ <- liftIO $ sendPresence (presenceSubscribed user) sess
|
||||||
|
liftIO $ putStrLn $ "subscribed to " ++ (show user)
|
||||||
|
|
||||||
|
handleMessage :: Jid -> Jid -> String -> Message -> BotMonad ()
|
||||||
|
handleMessage _ fromUser "PING" _ =
|
||||||
|
sendMucMessage
|
||||||
|
"pong"
|
||||||
|
(toBare fromUser)
|
||||||
|
handleMessage _ fromUser "BING" _ =
|
||||||
|
sendMucMessage
|
||||||
|
"cannabis smoking device"
|
||||||
|
(toBare fromUser)
|
||||||
|
handleMessage _ fromUser ('.':'8':'B':'A':'L':'L':' ':_) o = do
|
||||||
|
rng <- gets stateRNG
|
||||||
|
let (x,rng') = randomR (0,(length responses)-1) rng
|
||||||
|
modify (\s -> s { stateRNG = rng' })
|
||||||
|
sendMucMessage
|
||||||
|
(T.pack ("> "
|
||||||
|
++ (drop 7 (T.unpack (fromMaybe ";0;" (getMucMessageBody o))))
|
||||||
|
++ "\n" ++ responses !! x))
|
||||||
|
(toBare fromUser)
|
||||||
|
where
|
||||||
|
responses =
|
||||||
|
[ "it is certain"
|
||||||
|
, "without a doubt"
|
||||||
|
, "you may rely on it"
|
||||||
|
, "yes definitely"
|
||||||
|
, "it is decidedly so"
|
||||||
|
, "as i see it, yes"
|
||||||
|
, "most likely"
|
||||||
|
, "yes"
|
||||||
|
, "outlook good"
|
||||||
|
, "signs point to yes"
|
||||||
|
, "reply hazy try again"
|
||||||
|
, "better not tell you now"
|
||||||
|
, "ask again later"
|
||||||
|
, "cannot predict now"
|
||||||
|
, "concentrate and ask again"
|
||||||
|
, "dont count on it"
|
||||||
|
, "outlook not so good"
|
||||||
|
, "my sources say no"
|
||||||
|
, "very doubtful"
|
||||||
|
, "my reply is no"
|
||||||
|
]
|
||||||
|
handleMessage _ fromUser _ _ | T.toUpper (fromMaybe "no" (resourcepart fromUser)) == "ELFIE" = do
|
||||||
|
rng <- gets stateRNG
|
||||||
|
let (x,rng') = randomR (0,21) rng :: (Int,StdGen)
|
||||||
|
modify (\s -> s { stateRNG = rng' })
|
||||||
|
if x == 13
|
||||||
|
then sendMucMessage "shut up smellfie" (toBare fromUser)
|
||||||
|
else liftIO $ return ()
|
||||||
|
handleMessage _ _ _ _ = liftIO $ return ()
|
25
src/Krobotkin/Monad.hs
Normal file
25
src/Krobotkin/Monad.hs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
|
module Krobotkin.Monad where
|
||||||
|
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.State
|
||||||
|
|
||||||
|
newtype BotMonad a = BotMonad
|
||||||
|
(ReaderT AppEnv (StateT AppState IO) a)
|
||||||
|
deriving
|
||||||
|
( Functor
|
||||||
|
, Applicative
|
||||||
|
, Monad
|
||||||
|
, MonadReader AppEnv
|
||||||
|
, MonadState AppState
|
||||||
|
, MonadIO
|
||||||
|
)
|
||||||
|
|
||||||
|
runBotMonad :: AppEnv -> AppState -> BotMonad a -> IO a
|
||||||
|
runBotMonad env appstate (BotMonad m) =
|
||||||
|
evalStateT (runReaderT m env) appstate
|
80
src/Krobotkin/Muc.hs
Normal file
80
src/Krobotkin/Muc.hs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Krobotkin.Muc where
|
||||||
|
|
||||||
|
import Krobotkin.Types
|
||||||
|
import Krobotkin.Monad
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
import Data.XML.Types
|
||||||
|
|
||||||
|
import Control.Monad.Reader
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
comeOnline :: BotMonad ()
|
||||||
|
comeOnline = do
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
liftIO $ sendPresence presenceOnline sess >> return ()
|
||||||
|
|
||||||
|
joinMuc :: MucInfo -> BotMonad ()
|
||||||
|
joinMuc muc = do
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
me <- liftIO $ getJid sess
|
||||||
|
liftIO $ putStrLn $ "joining muc: " ++ show (mucJidNick muc)
|
||||||
|
case me of
|
||||||
|
Nothing -> liftIO $ putStrLn "error: failed to get jid from session"
|
||||||
|
Just n -> do
|
||||||
|
liftIO $ sendPresence
|
||||||
|
(Presence
|
||||||
|
{ presenceID = Nothing
|
||||||
|
, presenceFrom = Just n
|
||||||
|
, presenceTo = Just $ mucJidNick muc
|
||||||
|
, presenceLangTag = Nothing
|
||||||
|
, presenceType = Available
|
||||||
|
, presencePayload = [(
|
||||||
|
Element
|
||||||
|
{ elementName = "{http://jabber.org/protocol/muc}x"
|
||||||
|
, elementAttributes = []
|
||||||
|
, elementNodes = []
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
, presenceAttributes = []
|
||||||
|
})
|
||||||
|
sess >> return ()
|
||||||
|
|
||||||
|
sendMucMessage :: T.Text -> Jid -> BotMonad ()
|
||||||
|
sendMucMessage content muc = do
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
me <- liftIO $ getJid sess
|
||||||
|
stid <- liftIO $ newStanzaID sess
|
||||||
|
liftIO $ sendMessage
|
||||||
|
(Message
|
||||||
|
{ messageID = Just stid
|
||||||
|
, messageFrom = me
|
||||||
|
, messageTo = Just muc
|
||||||
|
, messageLangTag = Nothing
|
||||||
|
, messageType = GroupChat
|
||||||
|
, messageAttributes = []
|
||||||
|
, messagePayload = [(
|
||||||
|
Element {
|
||||||
|
elementName = "body"
|
||||||
|
, elementAttributes = []
|
||||||
|
, elementNodes = [
|
||||||
|
(NodeContent $ ContentText content)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
})
|
||||||
|
sess >> return ()
|
||||||
|
|
||||||
|
getMucMessageBody :: Message -> Maybe T.Text
|
||||||
|
getMucMessageBody m =
|
||||||
|
if elements == []
|
||||||
|
then Nothing
|
||||||
|
else case head (elementNodes $ head elements) of
|
||||||
|
(NodeContent (ContentText content)) -> Just content
|
||||||
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
elements = filter (\p -> (nameLocalName (elementName p)) == "body") pl
|
||||||
|
pl = messagePayload m
|
44
src/Krobotkin/Options.hs
Normal file
44
src/Krobotkin/Options.hs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
module Krobotkin.Options where
|
||||||
|
|
||||||
|
import Options.Applicative
|
||||||
|
|
||||||
|
data Flags = Flags
|
||||||
|
{ flagUsername :: String
|
||||||
|
, flagServer :: String
|
||||||
|
, flagPassword :: String
|
||||||
|
, flagMucs :: [String]
|
||||||
|
} deriving Show
|
||||||
|
|
||||||
|
options :: Parser Flags
|
||||||
|
options = Flags
|
||||||
|
<$> strOption
|
||||||
|
( long "username"
|
||||||
|
<> short 'u'
|
||||||
|
<> metavar "USERNAME"
|
||||||
|
<> help "xmpp username" )
|
||||||
|
<*> strOption
|
||||||
|
( long "server"
|
||||||
|
<> short 's'
|
||||||
|
<> metavar "SERVER"
|
||||||
|
<> help "xmpp server" )
|
||||||
|
<*> strOption
|
||||||
|
( long "password"
|
||||||
|
<> short 'p'
|
||||||
|
<> metavar "PASSWORD"
|
||||||
|
<> help "account password" )
|
||||||
|
<*> multiString
|
||||||
|
( long "mucs"
|
||||||
|
<> short 'm'
|
||||||
|
<> metavar "MUC"
|
||||||
|
<> help "muc to join" )
|
||||||
|
|
||||||
|
multiString :: Mod OptionFields [String] -> Parser [String]
|
||||||
|
multiString desc = concat <$> many single
|
||||||
|
where
|
||||||
|
single = option (str >>= return . words) desc
|
||||||
|
|
||||||
|
getOptions :: ParserInfo Flags
|
||||||
|
getOptions = info (options <**> helper)
|
||||||
|
( fullDesc
|
||||||
|
<> header "krobotkin - an xmpp muc bot"
|
||||||
|
<> progDesc "a friendly bot for your xmpp mucs :)" )
|
53
src/Krobotkin/Setup.hs
Normal file
53
src/Krobotkin/Setup.hs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Krobotkin.Setup where
|
||||||
|
|
||||||
|
import Krobotkin.Options
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
|
||||||
|
import Data.Maybe
|
||||||
|
import System.Random
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
-- TODO: have this report errors
|
||||||
|
createMucInfo :: Flags -> [MucInfo]
|
||||||
|
createMucInfo opts = catMaybes $ map
|
||||||
|
(packMucInfo $ T.pack (flagUsername opts))
|
||||||
|
(map T.pack (flagMucs opts))
|
||||||
|
|
||||||
|
packMucInfo :: T.Text -> T.Text -> Maybe MucInfo
|
||||||
|
packMucInfo username muc =
|
||||||
|
case jidToTexts <$> jidFromText muc of
|
||||||
|
Nothing -> Nothing
|
||||||
|
Just (user, server, resource) ->
|
||||||
|
let n = jidFromTexts user server $ Just $ T.replace "_" " " (fromMaybe username resource)
|
||||||
|
in case n of
|
||||||
|
Nothing -> Nothing
|
||||||
|
Just o -> Just $ MucInfo (toBare o) o
|
||||||
|
|
||||||
|
createXmppSession :: Flags -> IO Session
|
||||||
|
createXmppSession opts = do
|
||||||
|
sess <- session server login config
|
||||||
|
case sess of
|
||||||
|
Left e -> error $ "xmpp-failure: " ++ show e
|
||||||
|
Right s -> return s
|
||||||
|
where
|
||||||
|
username = T.pack $ flagUsername opts
|
||||||
|
password = T.pack $ flagPassword opts
|
||||||
|
server = flagServer opts
|
||||||
|
login = (Just (\_ -> ([scramSha1 username Nothing password]), Nothing))
|
||||||
|
config = def { keepAlive = Just 60 }
|
||||||
|
|
||||||
|
createEnv :: Flags -> IO AppEnv
|
||||||
|
createEnv opts = do
|
||||||
|
sess <- createXmppSession opts
|
||||||
|
return $ AppEnv sess (createMucInfo opts)
|
||||||
|
|
||||||
|
createState :: IO AppState
|
||||||
|
createState = do
|
||||||
|
rng <- getStdGen
|
||||||
|
return $ AppState rng
|
||||||
|
|
35
src/Krobotkin/Stanza.hs
Normal file
35
src/Krobotkin/Stanza.hs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
module Krobotkin.Stanza where
|
||||||
|
|
||||||
|
import Krobotkin.Muc
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
import Network.Xmpp.Internal
|
||||||
|
|
||||||
|
import Data.Char
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
packStanza :: Stanza -> PackedStanza
|
||||||
|
packStanza (MessageS m) = packMessage m
|
||||||
|
packStanza (PresenceS p) = packPresence p
|
||||||
|
packStanza _ = BotIgnore
|
||||||
|
|
||||||
|
packPresence :: Presence -> PackedStanza
|
||||||
|
packPresence p =
|
||||||
|
case (presenceFrom p) of
|
||||||
|
Just f -> BotPresence f (presenceType p) p
|
||||||
|
_ -> BotIgnore
|
||||||
|
|
||||||
|
packMessage :: Message -> PackedStanza
|
||||||
|
packMessage m =
|
||||||
|
let toUser = messageTo m
|
||||||
|
fromUser = messageFrom m
|
||||||
|
in case (messageType m) of
|
||||||
|
GroupChat -> case getMucMessageBody m of
|
||||||
|
Nothing -> BotIgnore
|
||||||
|
Just b -> case (toUser,fromUser) of
|
||||||
|
(Just t, Just f) -> BotMessage t f (map toUpper (T.unpack b)) m
|
||||||
|
_ -> BotIgnore
|
||||||
|
_ -> BotIgnore
|
||||||
|
|
24
src/Krobotkin/Types.hs
Normal file
24
src/Krobotkin/Types.hs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module Krobotkin.Types where
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
import System.Random
|
||||||
|
|
||||||
|
data AppEnv = AppEnv
|
||||||
|
{ envXmppSess :: Session
|
||||||
|
, envMucInfo :: [MucInfo]
|
||||||
|
}
|
||||||
|
|
||||||
|
data MucInfo = MucInfo
|
||||||
|
{ mucJid :: Jid
|
||||||
|
, mucJidNick :: Jid
|
||||||
|
}
|
||||||
|
|
||||||
|
data AppState = AppState
|
||||||
|
{ stateRNG :: StdGen
|
||||||
|
}
|
||||||
|
|
||||||
|
data PackedStanza =
|
||||||
|
BotPresence Jid PresenceType Presence
|
||||||
|
| BotMessage Jid Jid String Message
|
||||||
|
| BotIgnore
|
||||||
|
|
31
src/Main.hs
Normal file
31
src/Main.hs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Krobotkin.Handlers
|
||||||
|
import Krobotkin.Monad
|
||||||
|
import Krobotkin.Muc
|
||||||
|
import Krobotkin.Options
|
||||||
|
import Krobotkin.Setup
|
||||||
|
import Krobotkin.Stanza
|
||||||
|
import Krobotkin.Types
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Reader
|
||||||
|
|
||||||
|
import Network.Xmpp
|
||||||
|
import Options.Applicative
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
opts <- execParser getOptions
|
||||||
|
env <- createEnv opts
|
||||||
|
state <- createState
|
||||||
|
runBotMonad env state $ do
|
||||||
|
mucs <- asks envMucInfo
|
||||||
|
mapM_ joinMuc mucs
|
||||||
|
mapM_ (sendMucMessage "greetings, comrades!") $ map mucJid mucs
|
||||||
|
sess <- asks envXmppSess
|
||||||
|
forever $ do
|
||||||
|
(st,_) <- liftIO $ getStanza sess
|
||||||
|
dispatchStanza $ packStanza st
|
Loading…
Reference in a new issue