packages feed

lambdabot-telegram-plugins-0.2.0: src/Lambdabot/Plugin/Telegram/Callback.hs

{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE OverloadedStrings #-}
module Lambdabot.Plugin.Telegram.Callback where

import Control.Exception.Lifted ( SomeException (..) )
import Control.Exception.Lifted as E (catch)
import Control.Monad.State (gets, lift)
import Data.List
import Data.List.Split
import qualified Data.Text as Text
import qualified Data.Map.Strict as Map
import Text.EditDistance
import Text.Regex.TDFA

import Lambdabot.Bot
import Lambdabot.Command
import Lambdabot.Config
import Lambdabot.IRC
import Lambdabot.Logging
import Lambdabot.Message
import Lambdabot.Monad
import Lambdabot.Nick
import Lambdabot.Plugin.Core
import Lambdabot.Util

import Lambdabot.Plugin.Telegram.Shared
import Lambdabot.Plugin.Telegram.Message


-- | In order to read messages from a different plugin, it is necessary
-- to set a callback with a known label. This function is a main entry point
-- as a plugin callback.
--
-- Since we needed an extended functionality from @eval@ plugin,
-- we used these non-exported functions from corresponding @lambdabot-haskell-plugins@ module.
doTGMSG :: IrcMessage -> Telegram ()
doTGMSG msg = do
  ignored     <- lift $ checkIgnore msg
  commands    <- getConfig commandPrefixes
  if ignored
    then doIGNORE msg
    else mapM_ (doTGMSG' commands (lambdabotName msg) msg) targets
  where
    alltargets = head (ircMsgParams msg)
    targets = map (parseNick (ircMsgServer msg)) $ splitOn "," alltargets

doIGNORE :: IrcMessage -> Telegram ()
doIGNORE = debugM . show

doTGMSG'
  :: [String] -- ^ Commands.
  -> Nick -- ^ My name.
  -> IrcMessage -- ^ IRC Message.
  -> Nick -- ^ Target name.
  -> Telegram ()
doTGMSG' commands myname msg target
    | myname == target
    = let (cmd, params) = splitFirstWord text
      in doPersonalMsg commands msg cmd params
    
    | flip any (":," :: String)
        $ \c -> (fmtNick (ircMsgServer msg) myname ++ [c]) `isPrefixOf` text
    = let Just wholeCmd = maybeCommand (fmtNick (ircMsgServer msg) myname) text
          (cmd, params) = splitFirstWord wholeCmd
      in doPublicMsg commands msg target cmd params
    
    | (commands `arePrefixesOf` text)
    && length text > 1
    && (text !! 1 /= ' ') -- elem of prefixes
    && (not (commands `arePrefixesOf` [text !! 1]) ||
      (length text > 2 && text !! 2 == ' ')) -- ignore @@ prefix, but not the @@ command itself
    = let (cmd, params) = splitFirstWord (dropWhile (==' ') text)
      in doPublicMsg commands msg target cmd params
    
    | otherwise = return () -- contextual messages are not allowed here
    
    where
        text = tail (head (tail (ircMsgParams msg)))

doPersonalMsg
  :: [String]
  -> IrcMessage
  -> String
  -> String
  -> Telegram ()
doPersonalMsg commands msg s r
  | commands `arePrefixesOf` s = doMsg msg (tail s) r who
  | otherwise                  = return () -- contextual messages are not allowed here
  where
    who = nick msg

doPublicMsg
  :: [String] -> IrcMessage -> Nick -> String -> String -> Telegram ()
doPublicMsg commands msg target s r
    | commands `arePrefixesOf` s  = doMsg msg (tail s) r target
    | otherwise                   = doIGNORE msg

-- | normal commands.
--
-- check privledges, do any spell correction, dispatch, handling
-- possible timeouts.
--
doMsg :: IrcMessage -> String -> String -> Nick -> Telegram ()
doMsg msg cmd rest towhere = do
    ldebug $ "doMsg : nick : " <> fmtNick "" towhere <> " : cmd : " <> cmd
    let ircmsg = tgIrcPrivMsg (getTgChatId msg) (getTgMsgId msg) . Text.pack
    allcmds <- lift (gets (Map.keys . ircCommands))
    let ms      = filter (isPrefixOf cmd) allcmds
    e <- getConfig editDistanceLimit
    case ms of
        [s] -> docmd msg towhere rest s                  -- a unique prefix
        _ | cmd `elem` ms -> docmd msg towhere rest cmd  -- correct command (usual case)
        _ | otherwise     -> case closests cmd allcmds of
          (n,[s]) | n < e ,  ms == [] -> docmd msg towhere rest s -- unique edit match
          (n,ss)  | n < e || ms /= []            -- some possibilities
              -> lift . ircmsg $ "Maybe you meant: "++showClean(nub(ms++ss))
          _   -> docmd msg towhere rest cmd         -- no prefix, edit distance too far

docmd :: IrcMessage -> Nick -> [Char] -> String -> Telegram ()
docmd msg towhere rest cmd' = lb $ 
    withCommand cmd'   -- Important.
        (tgIrcPrivMsg (getTgChatId msg) (getTgMsgId msg)  "Unknown command, try @list")
        (\theCmd -> do
            hasPrivs <- lb (checkPrivs msg)
            -- TODO: handle disabled commands earlier
            -- users should probably see no difference between a
            -- command that is disabled and one that doesn't exist.
            disabled <- elem cmd' <$> getConfig disabledCommands
            let ok = not disabled && (not (privileged theCmd) || hasPrivs)

            -- unfortunately we have to pre-process command here to add some context,
            -- since the 'process' accepts a 'String' but we want more info specified
            -- (e.g. 'ChatId') to create multiple sandboxes
            debugM $ "docmd : nick : " <> fmtNick "" towhere <> " : cmd : " <> cmd' <> " : input : " <> rest
            let new = if cmd' `elem` ["@run", "@define", "@undefine", "@let", "run", "define", "undefine", "let"]
                  then Text.unpack (getTgChatId msg) <> "|" <> rest
                  else rest

            response <- if not ok
                then return ["Not enough privileges"]
                else runCommand theCmd msg towhere cmd' new
                    `E.catch` \exc@SomeException{} ->
                        return ["Plugin `Telegram` failed with: " ++ show exc]
            
            lift $ mapM_ (tgIrcPrivMsg (getTgChatId msg) (getTgMsgId msg) . Text.pack . expandTab 8) response
        )


closests :: String -> [String] -> (Int,[String])
closests pat ss = Map.findMin m
  where
    m = Map.fromListWith (++) ls
    ls = [ (levenshteinDistance defaultEditCosts pat s, [s]) | s <- ss ]

maybeCommand :: String -> String -> Maybe String
maybeCommand nm text = mrAfter <$> matchM re text
  where
    re :: Regex
    re = makeRegex (nm ++ "[.:,]*[[:space:]]*")