Search code examples
haskellservant

Making a request inside an API endpoint in Servant


I'm trying to build a Telegram bot using telegram-api. I haven't had an issues with that so far since I could read the tests to understand how things worked, but I've had a lot of trouble when it comes to building a webhook endpoint with Servant. The overall idea is that when I receive an Update from the webhook, I send a reply back.

The problem, is to do with my postWebhook code, where it expects to receive a Message but instead receives an IO Message. I think this is because Servant isn't expecting me to make a request inside that function because I have the type EitherT ServantError IO (IO Message) (partially applied by BotHandler) when really it should be EitherT ServantError IO Message.

I'm still learning Haskell but I understand that I somehow have to get the message out of the IO monad? Updating BotAPI to return a Post '[JSON] (IO Message) gives me this: No instance for (Foldable IO) arising from a use of ‘serve’, which is getting beyond my beginner knowledge, and I can see that fiddling with the types just moves the same problem to a different part of the code. I just don't know how to work around it.

Here is the code with sensitive strings removed:

{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE DeriveAnyClass    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators     #-}

module Main where

import           Control.Monad
import           Control.Monad.Trans.Either
import           Data.Proxy
import           Data.Text                  (Text, pack)
import           Network.Wai.Handler.Warp
import           Servant
import           Web.Telegram.API.Bot

reply :: Token -> Message -> Text -> IO Message
reply token msg response = do
  Right MessageResponse { message_result = m } <-
    sendMessage token $ SendMessageRequest chatId response (Just Markdown) Nothing Nothing Nothing
  return m
  where chatId = pack . show $ chat_id (chat msg)

type BotAPI = "webhook" :> ReqBody '[JSON] Update :> Post '[JSON] Message
type BotHandler a = EitherT ServantErr IO a

botApi :: Proxy BotAPI
botApi = Proxy

initWebhook :: Token -> IO Bool
initWebhook token = do
  Right SetWebhookResponse { webhook_result = w } <-
    setWebhook token $ Just "https://example.com/webhook"
  return w

postWebhook :: Token -> Update -> BotHandler (IO Message)
postWebhook token update = case message update of
  Just msg -> return $ reply token msg "Testing!"
  Nothing -> left err400

server :: Token -> Server BotAPI
server = postWebhook

main :: IO ()
main = do
  initWebhook token
  run port $ serve botApi (server token)
  where token = Token "<token>"
        port = 8080

Apologies for what might not be ideal Haskell code. Thank you in advance :)


Solution

  • Your error is at

    Just msg → return $ reply token msg "Testing!"
    

    you are on

    EitherT ServantErr IO Message
    

    monad but reply has type

    reply :: Token → Message → Text → IO Message
    

    then simply lift that IO action into your monad, it works

    postWebhook :: Token → Update → BotHandler Message
    postWebhook token update = case message update of
      Just msg → lift $ reply token msg "Testing!"
      Nothing  → left err400
    

    (Is not easy to me explain all things involved here) I think you should practice more about monads, transformers, ... before these complex examples but you're brave! :)