{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} module Lambdabot.Plugin.Telegram.Bot where import Control.Monad import Control.Monad.State import Data.Char import Data.Coerce import Data.Maybe import qualified Data.Text as Text import Data.Text (Text) import GHC.Generics import Telegram.Bot.API import Telegram.Bot.Simple import Telegram.Bot.Simple.UpdateParser import Text.Read (readMaybe) import Lambdabot.Plugin.Telegram.Shared import Lambdabot.Plugin.Telegram.Bot.Generic -- | Telegram Model. type Model = TelegramState -- | Supported actions: -- * send everything obtained from user to lambdabot ("proxy" command). -- * send exact module command to lambdabot. -- * send response back to user. data Action = SendEverything Msg | SendModule ModuleCmd | SendBack Msg -- | Supported modules. data ModuleCmd = EvalModule EvalCmd | CheckModule CheckCmd | DjinnModule DjinnCmd | FreeModule FreeCmd | HaddockModule HaddockCmd | HoogleModule HoogleCmd | InstancesModule InstancesCmd | PlModule PlCmd | PointfulModule PointfulCmd | PrettyModule PrettyCmd | SystemModule SystemCmd | TypeModule TypeCmd | UndoModule UndoCmd | UnmtlModule UnmtlCmd | VersionModule VersionCmd | HelpModule HelpCmd | SourceModule SourceCmd -- | Supported commands from @eval@ plugin. data EvalCmd = Let Msg | Undefine Msg | Run Msg deriving (Generic, FromCommand) -- | Supported commands from @check@ plugin. data CheckCmd = Check Msg deriving (Generic, FromCommand) -- | Supported commands from @djinn@ plugin. data DjinnCmd = Djinn Msg | DjinnAdd Msg | DjinnDel Msg | DjinnEnv Msg | DjinnNames Msg | DjinnClr Msg | DjinnVer Msg deriving (Generic, FromCommand) -- | Supported commands from @free@ plugin. data FreeCmd = Free Msg deriving (Generic, FromCommand) -- | Supported commands from @haddock@ plugin. data HaddockCmd = Index Msg deriving (Generic, FromCommand) -- | Supported commands from @hoogle@ plugin. data HoogleCmd = Hoogle Msg deriving (Generic, FromCommand) -- | Supported commands from @instances@ plugin. data InstancesCmd = Instances Msg | InstancesImporting Msg deriving (Generic, FromCommand) -- | Supported commands from @pl@ plugin. data PlCmd = Pl Msg | PlResume Msg deriving (Generic, FromCommand) -- | Supported commands from @pointful@ plugin. data PointfulCmd = Pointful Msg | Pointy Msg | Repoint Msg | Unpointless Msg | Unpl Msg | Unpf Msg deriving (Generic, FromCommand) -- | Supported commands from @pretty@ plugin. data PrettyCmd = Pretty Msg deriving (Generic, FromCommand) -- | Supported commands from @system@ plugin. data SystemCmd = Listchans Msg | Listmodules Msg | Listservers Msg | List Msg | Echo Msg | Uptime Msg deriving (Generic, FromCommand) -- | Supported commands from @type@ plugin. data TypeCmd = Type Msg | Kind Msg deriving (Generic, FromCommand) -- | Supported commands from @undo@ plugin. data UndoCmd = Undo Msg | Do Msg deriving (Generic, FromCommand) -- | Supported commands from @unmtl@ plugin. data UnmtlCmd = Unmtl Msg deriving (Generic, FromCommand) -- | Supported commands from @version@ plugin. data VersionCmd = Tgversion Msg deriving (Generic, FromCommand) -- | Supported commands from @help@ plugin. data HelpCmd = Help Msg deriving (Generic, FromCommand) -- | Supported commands from @source@ plugin. data SourceCmd = Src Msg deriving (Generic, FromCommand) -- | The bot. telegramLambdaBot :: TelegramState -> BotApp Model Action telegramLambdaBot tgstate = BotApp { botInitialModel = tgstate , botAction = flip updateToAction , botHandler = handleAction , botJobs = [] } -- | How to handle updates from Telegram. updateToAction :: Model -> Update -> Maybe Action updateToAction TelegramState{..} update -- proxy command | isCommand "irc" update = SendEverything <$> updateToMsg update -- eval commands | isCommand "let" update = SendModule <$> (EvalModule <$> (Let <$> updateToMsg update)) | isCommand "run" update = SendModule <$> (EvalModule <$> (Run <$> updateToMsg update)) | isCommand "define" update = SendModule <$> (EvalModule <$> (Let <$> updateToMsg update)) | isCommand "undefine" update = SendModule <$> (EvalModule <$> (Undefine <$> updateToMsg update)) -- check commands | isCommand "check" update = SendModule <$> (CheckModule <$> (Check <$> updateToMsg update)) -- djinn commands | isCommand "djinn" update = SendModule <$> (DjinnModule <$> (Djinn <$> updateToMsg update)) | isCommand "djinnadd" update = SendModule <$> (DjinnModule <$> (DjinnAdd <$> updateToMsg update)) | isCommand "djinndel" update = SendModule <$> (DjinnModule <$> (DjinnDel <$> updateToMsg update)) | isCommand "djinnenv" update = SendModule <$> (DjinnModule <$> (DjinnEnv <$> updateToMsg update)) | isCommand "djinnnames" update = SendModule <$> (DjinnModule <$> (DjinnNames <$> updateToMsg update)) | isCommand "djinnclr" update = SendModule <$> (DjinnModule <$> (DjinnClr <$> updateToMsg update)) | isCommand "djinnver" update = SendModule <$> (DjinnModule <$> (DjinnVer <$> updateToMsg update)) -- free commands | isCommand "free" update = SendModule <$> (FreeModule <$> (Free <$> updateToMsg update)) -- haddock | isCommand "index" update = SendModule <$> (HaddockModule <$> (Index <$> updateToMsg update)) -- hoogle | isCommand "hoogle" update = SendModule <$> (HoogleModule <$> (Hoogle <$> updateToMsg update)) -- instances | isCommand "instances" update = SendModule <$> (InstancesModule <$> (Instances <$> updateToMsg update)) | isCommand "instancesimporting" update = SendModule <$> (InstancesModule <$> (InstancesImporting <$> updateToMsg update)) -- pl | isCommand "pl" update = SendModule <$> (PlModule <$> (Pl <$> updateToMsg update)) | isCommand "plresume" update = SendModule <$> (PlModule <$> (PlResume <$> updateToMsg update)) -- pointful | isCommand "pointful" update = SendModule <$> (PointfulModule <$> (Pointful <$> updateToMsg update)) | isCommand "pointy" update = SendModule <$> (PointfulModule <$> (Pointy <$> updateToMsg update)) | isCommand "repoint" update = SendModule <$> (PointfulModule <$> (Repoint <$> updateToMsg update)) | isCommand "unpointless" update = SendModule <$> (PointfulModule <$> (Unpointless <$> updateToMsg update)) | isCommand "unpl" update = SendModule <$> (PointfulModule <$> (Unpl <$> updateToMsg update)) | isCommand "unpf" update = SendModule <$> (PointfulModule <$> (Unpf <$> updateToMsg update)) -- pretty | isCommand "pretty" update = SendModule <$> (PrettyModule <$> (Pretty <$> updateToMsg update)) -- system -- FIXME: decide about `listchans`, `listservers` | isCommand "listmodules" update = SendModule <$> (SystemModule <$> (Listmodules <$> updateToMsg update)) | isCommand "list" update = SendModule <$> (SystemModule <$> (List <$> updateToMsg update)) | isCommand "echo" update = SendModule <$> (SystemModule <$> (Echo <$> updateToMsg update)) | isCommand "uptime" update = SendModule <$> (SystemModule <$> (Uptime <$> updateToMsg update)) -- type | isCommand "type" update = SendModule <$> (TypeModule <$> (Type <$> updateToMsg update)) | isCommand "kind" update = SendModule <$> (TypeModule <$> (Kind <$> updateToMsg update)) -- undo | isCommand "undo" update = SendModule <$> (UndoModule <$> (Undo <$> updateToMsg update)) | isCommand "do" update = SendModule <$> (UndoModule <$> (Do <$> updateToMsg update)) -- unmtl | isCommand "unmtl" update = SendModule <$> (UnmtlModule <$> (Unmtl <$> updateToMsg update)) -- version | isCommand "version" update = SendModule <$> (VersionModule <$> (Tgversion <$> updateToMsg update)) -- help | isCommand "help" update = SendModule <$> (HelpModule <$> (Help <$> updateToMsg update)) -- source | isCommand "src" update = SendModule <$> (SourceModule <$> (Src <$> updateToMsg update)) | otherwise = Nothing where isCommand cmd = isJust . parseUpdate (commandWithBotName tgBotName cmd) dropCommand = Text.dropWhile isSpace . Text.dropWhile (not . isSpace) intToText :: Coercible a Integer => a -> Text intToText = Text.pack . show . coerce @_ @Integer updateToMsg upd = Msg <$> (fmap intToText . updateChatId) upd <*> (fmap (intToText . messageMessageId) . extractUpdateMessage) upd <*> (fmap dropCommand . updateMessageText) upd -- | Extract 'Msg' from incoming command and send to Lambdabot. handlePluginCommand :: FromCommand cmd => cmd -> Model -> Eff Action Model handlePluginCommand cmd model = model <# do liftIO $ writeInput (fromCommand cmd) model return () -- | How to handle module 'Action'. handleModuleAction :: ModuleCmd -> Model -> Eff Action Model handleModuleAction (EvalModule cmd) model = handlePluginCommand cmd model handleModuleAction (CheckModule cmd) model = handlePluginCommand cmd model handleModuleAction (DjinnModule cmd) model = handlePluginCommand cmd model handleModuleAction (FreeModule cmd) model = handlePluginCommand cmd model handleModuleAction (HaddockModule cmd) model = handlePluginCommand cmd model handleModuleAction (HoogleModule cmd) model = handlePluginCommand cmd model handleModuleAction (InstancesModule cmd) model = handlePluginCommand cmd model handleModuleAction (PlModule cmd) model = handlePluginCommand cmd model handleModuleAction (PointfulModule cmd) model = handlePluginCommand cmd model handleModuleAction (PrettyModule cmd) model = handlePluginCommand cmd model handleModuleAction (SystemModule cmd) model = handlePluginCommand cmd model handleModuleAction (TypeModule cmd) model = handlePluginCommand cmd model handleModuleAction (UndoModule cmd) model = handlePluginCommand cmd model handleModuleAction (UnmtlModule cmd) model = handlePluginCommand cmd model handleModuleAction (VersionModule cmd) model = handlePluginCommand cmd model handleModuleAction (HelpModule cmd) model = case (msgMessage $ getMessage cmd) of "" -> model <# do pure $ SendBack $ Msg { msgChatId = msgChatId $ getMessage cmd , msgMsgId = msgMsgId $ getMessage cmd , msgMessage = helpCmd } _ -> handlePluginCommand cmd model handleModuleAction (SourceModule cmd) model = handlePluginCommand cmd model -- | How to handle 'Action'. handleAction :: Action -> Model -> Eff Action Model handleAction (SendEverything msg) model = model <# do liftIO $ writeInput msg model return () handleAction (SendModule moduleCmd) model = handleModuleAction moduleCmd model handleAction (SendBack msg) model = model <# do let Msg chatIdText msgIdText response = msg parseChatId = fmap ChatId . readMaybe . Text.unpack parseMsgId = fmap MessageId . readMaybe . Text.unpack mchatId = parseChatId chatIdText mreplyMessageId = parseMsgId msgIdText case mchatId of Nothing -> pure () Just tgchatId -> do let req = SendMessageRequest { sendMessageChatId = SomeChatId tgchatId , sendMessageText = "```\n" <> response <> "\n```\n" , sendMessageParseMode = Just MarkdownV2 , sendMessageEntities = Nothing , sendMessageDisableWebPagePreview = Nothing , sendMessageDisableNotification = Nothing , sendMessageProtectContent = Nothing , sendMessageReplyToMessageId = mreplyMessageId , sendMessageAllowSendingWithoutReply = Nothing , sendMessageReplyMarkup = Nothing } _ <- liftClientM (sendMessage req) pure () -- | Run Telegram bot. runTelegramBot :: Token -> TelegramState -> IO () runTelegramBot token tgstate = do env <- defaultTelegramClientEnv token botActionFun <- startBotAsync (telegramLambdaBot tgstate) env forever $ do response <- readOutput tgstate botActionFun (SendBack response) -- | Help command text. helpCmd :: Text helpCmd = "Lambdabot for Telegram provides following plugins:\n\ \\n\ \telegram check djinn free haddock hoogle instances pl pointful pretty source system type undo unmtl\n\ \\n\ \telegram plugin has following commands:\n\ \\n\ \- /version - version/source. Report the version and git repo of this bot\n\ \- /run - run . You have Haskell, 3 seconds and no IO. Go nuts!\n\ \- /let - let = . Add a binding.\n\ \- /define - let = . Add a binding.\n\ \- /undefine - undefine. Reset evaluator local bindings.\n\ \\n\ \check plugin has following command:\n\ \\n\ \- /check - check . You have QuickCheck and 3 seconds. Prove something.\n\ \\n\ \djinn plugin has following commands:\n\ \\n\ \- /djinn - djinn . Generates Haskell code from a type.\n\ \- /djinnadd - djinn-add . Define a new function type or type synonym.\n\ \- /djinndel - djinn-del . Remove a symbol from the environment.\n\ \- /djinnenv - Show the current djinn environment.\n\ \- /djinnnames - Show the current djinn environment, compactly.\n\ \- /djinnclr - Reset the djinn environment.\n\ \- /djinnver - Show current djinn version.\n\ \\n\ \free plugin has following command:\n\ \\n\ \- /free - free . Generate theorems for free.\n\ \\n\ \haddock plugin has following command:\n\ \\n\ \- /index - index . Returns the Haskell modules in which is defined.\n\ \\n\ \hoogle plugin has following command:\n\ \\n\ \- /hoogle - hoogle . Haskell API Search for either names, or types.\n\ \\n\ \instances plugin has following commands:\n\ \\n\ \- /instances - instances . Fetch the instances of a typeclass.\n\ \- /instancesimporting - instancesimporting [ [ [. Fetch the instances of a typeclass, importing specified modules first.\n\ \\n\ \pl plugin has following command:\n\ \\n\ \- /pl - pointless . Play with pointfree code.\n\ \\n\ \pointful plugin has following commands:\n\ \\n\ \- /pointy - pointful . Make code pointier.\n\ \- /repoint - pointful . Make code pointier.\n\ \- /unpointless - pointful . Make code pointier.\n\ \- /unpl - pointful . Make code pointier.\n\ \- /unpf - pointful . Make code pointier.\n\ \\n\ \pretty plugin has following commands:\n\ \\n\ \- /pretty - pretty . Display haskell code in a pretty-printed manner\n\ \\n\ \type plugin has following commands:\n\ \\n\ \- /type - type . Return the type of a value.\n\ \- /kind - kind . Return the kind of a type.\n\ \\n\ \source plugin has following commands:\n\ \- /src - src . Display the implementation of a standard function.\n\ \\n\ \undo plugin has following commands:\n\ \\n\ \- /undo - undo . Translate do notation to Monad operators.\n\ \- /do - do . Translate Monad operators to do notation.\n\ \\n\ \unmtl has following commands:\n\ \\n\ \- /unmtl - unroll mtl monads.\n\ \\n\ \Other commands:\n\ \- /help - shows this help.\n\ \- /version - version/source. Report the version and git repo of this bot\n\ \\n\ \All plugins are independent from each other, i.e. have their own state or use different programs under the hood."