/src/XmppBot.hs

http://github.com/Eelis/geordi · Haskell · 103 lines · 86 code · 16 blank · 1 comment · 10 complexity · 548f304836e07a10cd62311416027905 MD5 · raw file

  1. {-# LANGUAGE UnicodeSyntax, PatternGuards #-}
  2. import qualified System.Environment
  3. import qualified XMPP
  4. import qualified MUC
  5. import qualified Sys
  6. import qualified Cxx.Show
  7. import qualified Request
  8. import qualified RequestEval
  9. import Data.Char (ord, toLower, toUpper)
  10. import System.Console.GetOpt (OptDescr(..), ArgDescr(..), ArgOrder(..), getOpt, usageInfo)
  11. import System.IO (putStrLn)
  12. import Control.Monad (guard, when, forM_, forever)
  13. import Prelude hiding (catch, (.), readFile, putStrLn, putStr, print)
  14. import Util
  15. type JID = String
  16. type Nick = String
  17. data RoomConfig = RoomConfig { room_nick :: Nick, allow_nickless_requests :: Bool } deriving Read
  18. data BotConfig = BotConfig
  19. { server, user, pass :: String
  20. , max_msg_length :: Int
  21. , no_output_msg :: String
  22. , blacklist :: [JID] -- These may include a resource part, so that room users may be specified.
  23. , rate_limit_messages, rate_limit_window :: Int
  24. , rooms :: [(JID, RoomConfig)]
  25. } deriving Read
  26. data Opt = Config String | Help deriving Eq
  27. optsDesc :: [OptDescr Opt]
  28. optsDesc =
  29. [ Option "c" ["config"] (ReqArg Config "<file>") "Load configuration from <file> instead of \"xmpp-config\"."
  30. , Option "h" ["help"] (NoArg Help) "Display this help and exit."
  31. ]
  32. help :: String
  33. help = usageInfo "Usage: sudo geordi-xmpp [option]...\nOptions:" optsDesc ++ "\nSee README.xhtml for more information."
  34. getArgs :: IO [Opt]
  35. getArgs = do
  36. args System.Environment.getArgs
  37. case getOpt RequireOrder optsDesc args of
  38. (_, _, err:_) fail $ init err
  39. (_, w:_, []) fail $ "superfluous command line argument: " ++ w
  40. (opts, [], []) return opts
  41. nicks_match :: Nick Nick Bool
  42. nicks_match n (h:t) = n == toLower h : t n == toUpper h : t
  43. nicks_match _ "" = error "empty nick"
  44. is_request :: RoomConfig String Maybe String
  45. is_request (RoomConfig mynick allow_nickless) s
  46. | Just (n, r) Request.is_addressed_request s, nicks_match n mynick = Just r
  47. | allow_nickless, Just r Request.is_short_request s = Just r
  48. | otherwise = Nothing
  49. output_body :: BotConfig Request.Response String
  50. output_body cfg r = xmlEntities $ take (max_msg_length cfg) $ takeWhile (/= '\n') $
  51. case Request.response_output r of "" no_output_msg cfg; s s
  52. main :: IO ()
  53. main = do
  54. Sys.setlocale_ALL_env
  55. opts getArgs
  56. if Help `elem` opts then putStrLn help else do
  57. cfg readTypedFile $ findMaybe (\o case o of Config cf Just cf; _ Nothing) opts `orElse` "xmpp-config"
  58. conn XMPP.openStream $ server cfg
  59. XMPP.getStreamStart conn
  60. evalRequest RequestEval.evaluator Cxx.Show.noHighlighting
  61. limit_rate Sys.rate_limiter (rate_limit_messages cfg) (rate_limit_window cfg)
  62. XMPP.runXMPP conn $ do
  63. XMPP.startAuth (user cfg) (server cfg) (pass cfg)
  64. XMPP.sendPresence
  65. XMPP.handleVersion "Geordi C++ bot - http://www.eelis.net/geordi/" "-" "-"
  66. forM_ (rooms cfg) $ \(jid, RoomConfig nick _) MUC.joinGroupchat nick jid
  67. forever $ do
  68. msg XMPP.waitForStanza (XMPP.isMessage .&&. XMPP.hasBody .&&. (not . isDelay))
  69. maybeM (XMPP.getAttr "from" msg) $ \from do
  70. when (not $ from `elem` blacklist cfg) $ do
  71. maybeM (XMPP.getMessageBody msg) $ \body do
  72. let
  73. eval r = XMPP.liftIO $ limit_rate >> output_body cfg . evalRequest r (Request.Context [])
  74. case XMPP.getAttr "type" msg of
  75. Just "groupchat"
  76. | (room, '/' : from_nick) span (/= '/') from, Just request do
  77. room_cfg lookup room $ rooms cfg
  78. guard $ room_nick room_cfg /= from_nick
  79. is_request room_cfg body
  80. eval request >>= MUC.sendGroupchatMessage room
  81. Just "chat" eval body >>= XMPP.sendMessage from
  82. Nothing eval body >>= XMPP.sendMessage from
  83. _ return ()
  84. xmlEntities :: String String
  85. xmlEntities = concatMap (\c "&#" ++ show (ord c) ++ ";")
  86. isDelay :: XMPP.StanzaPredicate
  87. isDelay = maybe False ((== Just "jabber:x:delay") . XMPP.getAttr "xmlns") . XMPP.xmlPath ["x"]