diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-13 00:44:27 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-13 00:44:27 -0500 |
| commit | 6bbf81f41c318a4200156e58707c807b230a601c (patch) | |
| tree | 68bb23ec635f6657ba6cb4c6804cfef44d90a4d5 /Omni | |
| parent | 4ff40843e7a6801b7785bfff7f4e9e8fff4e27d4 (diff) | |
telegram: add group chat support
- Only respond in groups when @mentioned or replied to
- Add ChatType to TelegramMessage (private/group/supergroup/channel)
- Add getMe API call to fetch bot username on startup
- Add shouldRespondInGroup helper function
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Agent/Telegram.hs | 43 | ||||
| -rw-r--r-- | Omni/Agent/Telegram/Types.hs | 47 |
2 files changed, 86 insertions, 4 deletions
diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index c55dc5a..ffad4c7 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -184,6 +184,29 @@ getUpdates cfg offset = do _ -> pure [] _ -> pure [] +getBotUsername :: Types.TelegramConfig -> IO (Maybe Text) +getBotUsername cfg = do + let url = + Text.unpack (Types.tgApiBaseUrl cfg) + <> "/bot" + <> Text.unpack (Types.tgBotToken cfg) + <> "/getMe" + result <- + try <| do + req <- HTTP.parseRequest url + HTTP.httpLBS req + case result of + Left (_ :: SomeException) -> pure Nothing + Right response -> do + let body = HTTP.getResponseBody response + case Aeson.decode body of + Just (Aeson.Object obj) -> case KeyMap.lookup "result" obj of + Just (Aeson.Object userObj) -> case KeyMap.lookup "username" userObj of + Just (Aeson.String username) -> pure (Just username) + _ -> pure Nothing + _ -> pure Nothing + _ -> pure Nothing + sendMessage :: Types.TelegramConfig -> Int -> Text -> IO () sendMessage cfg chatId text = do let url = @@ -231,6 +254,12 @@ runTelegramBot tgConfig provider = do putText "Starting Telegram bot..." offsetVar <- newTVarIO 0 + botUsername <- getBotUsername tgConfig + case botUsername of + Nothing -> putText "Warning: could not get bot username, group mentions may not work" + Just name -> putText <| "Bot username: @" <> name + let botName = fromMaybe "bot" botUsername + _ <- forkIO (reminderLoop tgConfig) putText "Reminder loop started (checking every 5 minutes)" @@ -249,28 +278,34 @@ runTelegramBot tgConfig provider = do messages <- getUpdates tgConfig offset forM_ messages <| \msg -> do atomically (writeTVar offsetVar (Types.tmUpdateId msg + 1)) - handleMessage tgConfig provider engineCfg msg + handleMessage tgConfig provider engineCfg botName msg when (null messages) <| threadDelay 1000000 handleMessage :: Types.TelegramConfig -> Provider.Provider -> Engine.EngineConfig -> + Text -> Types.TelegramMessage -> IO () -handleMessage tgConfig provider engineCfg msg = do +handleMessage tgConfig provider engineCfg botUsername msg = do let userName = Types.tmUserFirstName msg <> maybe "" (" " <>) (Types.tmUserLastName msg) chatId = Types.tmChatId msg usrId = Types.tmUserId msg + unless (Types.shouldRespondInGroup botUsername msg) <| do + when (Types.isGroupChat msg) + <| putText + <| "Ignoring group message (not mentioned): " + <> Text.take 50 (Types.tmText msg) + unless (Types.isUserAllowed tgConfig usrId) <| do putText <| "Unauthorized user: " <> tshow usrId <> " (" <> userName <> ")" sendMessage tgConfig chatId "sorry, you're not authorized to use this bot." - pure () - when (Types.isUserAllowed tgConfig usrId) <| do + when (Types.shouldRespondInGroup botUsername msg && Types.isUserAllowed tgConfig usrId) <| do sendTypingAction tgConfig chatId user <- Memory.getOrCreateUserByTelegramId usrId userName diff --git a/Omni/Agent/Telegram/Types.hs b/Omni/Agent/Telegram/Types.hs index 2db6a52..d240786 100644 --- a/Omni/Agent/Telegram/Types.hs +++ b/Omni/Agent/Telegram/Types.hs @@ -19,6 +19,7 @@ module Omni.Agent.Telegram.Types TelegramPhoto (..), TelegramVoice (..), TelegramReplyMessage (..), + ChatType (..), -- * Parsing parseUpdate, @@ -31,6 +32,8 @@ module Omni.Agent.Telegram.Types -- * Utilities isPdf, isSupportedVoiceFormat, + isGroupChat, + shouldRespondInGroup, -- * Testing main, @@ -81,6 +84,7 @@ test = TelegramMessage { tmUpdateId = 123, tmChatId = 456, + tmChatType = Private, tmUserId = 789, tmUserFirstName = "Test", tmUserLastName = Just "User", @@ -319,9 +323,28 @@ instance Aeson.FromJSON TelegramReplyMessage where <*> (v .:? "from_last_name") <*> (v .:? "text" .!= "") +data ChatType = Private | Group | Supergroup | Channel + deriving (Show, Eq, Generic) + +instance Aeson.ToJSON ChatType where + toJSON Private = Aeson.String "private" + toJSON Group = Aeson.String "group" + toJSON Supergroup = Aeson.String "supergroup" + toJSON Channel = Aeson.String "channel" + +instance Aeson.FromJSON ChatType where + parseJSON = Aeson.withText "ChatType" parseChatType + where + parseChatType "private" = pure Private + parseChatType "group" = pure Group + parseChatType "supergroup" = pure Supergroup + parseChatType "channel" = pure Channel + parseChatType _ = pure Private + data TelegramMessage = TelegramMessage { tmUpdateId :: Int, tmChatId :: Int, + tmChatType :: ChatType, tmUserId :: Int, tmUserFirstName :: Text, tmUserLastName :: Maybe Text, @@ -338,6 +361,7 @@ instance Aeson.ToJSON TelegramMessage where Aeson.object [ "update_id" .= tmUpdateId m, "chat_id" .= tmChatId m, + "chat_type" .= tmChatType m, "user_id" .= tmUserId m, "user_first_name" .= tmUserFirstName m, "user_last_name" .= tmUserLastName m, @@ -353,6 +377,7 @@ instance Aeson.FromJSON TelegramMessage where Aeson.withObject "TelegramMessage" <| \v -> (TelegramMessage </ (v .: "update_id")) <*> (v .: "chat_id") + <*> (v .:? "chat_type" .!= Private) <*> (v .: "user_id") <*> (v .: "user_first_name") <*> (v .:? "user_last_name") @@ -385,6 +410,12 @@ parseUpdate val = do chatId <- case KeyMap.lookup "id" chatObj of Just (Aeson.Number n) -> Just (round n) _ -> Nothing + let chatType = case KeyMap.lookup "type" chatObj of + Just (Aeson.String "private") -> Private + Just (Aeson.String "group") -> Group + Just (Aeson.String "supergroup") -> Supergroup + Just (Aeson.String "channel") -> Channel + _ -> Private Aeson.Object fromObj <- KeyMap.lookup "from" msgObj userId <- case KeyMap.lookup "id" fromObj of Just (Aeson.Number n) -> Just (round n) @@ -419,6 +450,7 @@ parseUpdate val = do TelegramMessage { tmUpdateId = updateId, tmChatId = chatId, + tmChatType = chatType, tmUserId = userId, tmUserFirstName = firstName, tmUserLastName = lastName, @@ -547,3 +579,18 @@ isSupportedVoiceFormat voice = Just "audio/x-opus+ogg" -> True Nothing -> True _ -> False + +isGroupChat :: TelegramMessage -> Bool +isGroupChat msg = tmChatType msg `elem` [Group, Supergroup] + +shouldRespondInGroup :: Text -> TelegramMessage -> Bool +shouldRespondInGroup botUsername msg + | not (isGroupChat msg) = True + | isMentioned = True + | isReplyToBot = True + | otherwise = False + where + msgText = Text.toLower (tmText msg) + mention = "@" <> Text.toLower botUsername + isMentioned = mention `Text.isInfixOf` msgText + isReplyToBot = isJust (tmReplyTo msg) |
