Skip to content

Instantly share code, notes, and snippets.

@paavohuhtala
Created December 6, 2016 12:42
Show Gist options
  • Save paavohuhtala/fcf7c73a474ad39277e6b3de492d60ae to your computer and use it in GitHub Desktop.
Save paavohuhtala/fcf7c73a474ad39277e6b3de492d60ae to your computer and use it in GitHub Desktop.
F# telegram bot with Hopac
namespace FsBots
open System
open Hopac
type ChatMessage<'id, 'msg, 'user> = {
chatId: 'id
content: string
conversation: Conversation<'id, 'msg, 'user>
internalMessage: 'msg
}
and OutboundMessage<'id> = | SendMessage of recipient: 'id * text: string
and Conversation<'id, 'msg, 'user> = {
chatId: 'id
from: 'user
started: DateTime
inbound: Mailbox<ChatMessage<'id, 'msg, 'user>>
outbound: Mailbox<OutboundMessage<'id>>
// Methods
send: string -> Job<unit>
receive: unit -> Job<ChatMessage<'id, 'msg, 'user>>
}
type ConversationHandler<'id, 'msg, 'user> =
| Handler of (ChatMessage<'id, 'msg, 'user> -> Job<ConversationHandler<'id, 'msg, 'user> option>)
type InternalMessage<'id, 'msg, 'user> = | MessageReceived of 'msg
type BotImpl<'id, 'msg, 'user when 'id : comparison> = {
sendMessage: ('id * string) -> Job<unit>
toChatMessage: Conversation<'id, 'msg, 'user> -> 'msg -> ChatMessage<'id, 'msg, 'user>
chatIdFromMessage: 'msg -> 'id
authorFromMessage: 'msg -> 'user
getInitialHandler: unit -> ConversationHandler<'id, 'msg, 'user>
}
and ServerState<'id, 'msg, 'user when 'id : comparison> =
{
bot: BotImpl<'id, 'msg, 'user>
serverMb: Mailbox<InternalMessage<'id, 'msg, 'user>>
outMb: Mailbox<OutboundMessage<'id>>
conversations: Map<'id, Conversation<'id, 'msg, 'user>>
initialHandler: ConversationHandler<'id, 'msg, 'user>
}
module Conversation =
let private send_ outbound chatId str = Mailbox.send outbound (SendMessage (chatId, str))
let private receive_ inbound = Mailbox.take inbound
let send (convo: Conversation<_, _, _>) str = send_ convo.outbound str
let receive (convo: Conversation<_, _, _>) = receive_ convo.inbound
let fromMessage impl outbound msg =
let inbound = Mailbox<ChatMessage<'id, 'msg, 'user>> ()
let chatId = impl.chatIdFromMessage msg
{
chatId = chatId
from = impl.authorFromMessage msg
started = DateTime.Now
inbound = Mailbox ()
outbound = outbound
send = send_ outbound chatId
receive = fun () -> receive_ inbound :> Job<_>
}
module Bot =
let rec convoLoop inbound (Handler handler) = job {
let! convoMsg = Mailbox.take inbound
let! nextHandler = handler convoMsg
do! convoLoop inbound (defaultArg nextHandler (Handler handler))
}
let rec serverLoop state = job {
let! msg = Mailbox.take state.serverMb
match msg with
| MessageReceived msg ->
let msgChatId = state.bot.chatIdFromMessage msg
let msgConvo = Map.tryFind msgChatId state.conversations
match msgConvo with
| Some convo ->
do! Mailbox.send convo.inbound (state.bot.toChatMessage convo msg)
return state
| None ->
let convo = Conversation.fromMessage state.bot state.outMb msg
let chatMsg = state.bot.toChatMessage convo msg
do! Job.start (convoLoop convo.inbound state.initialHandler)
do! Mailbox.send convo.inbound chatMsg
return { state with conversations = (Map.add convo.chatId convo state.conversations) }
}
let rec outboundServer state = job {
let! msg = Mailbox.take state.outMb
match msg with
| SendMessage (recipient, text) ->
do! state.bot.sendMessage (recipient, text)
return state
}
let public startNew impl =
let outMb = Mailbox ()
let state = {
bot = impl
serverMb = Mailbox ()
outMb = outMb
conversations = Map.empty
initialHandler = impl.getInitialHandler ()
}
job {
do! Job.iterateServer state outboundServer
do! Job.iterateServer state serverLoop
return state
}
namespace FsBots.Telegram
open Hopac
open Telegram.Bot
open Telegram.Bot.Types
open Telegram.Bot.Types.Enums
open FsBots
module TelegramBot =
let startNew token initialHandler =
let bot = TelegramBotClient(token)
let impl = {
sendMessage = fun (chatId: int64, msg: string) -> job {
do! Job.awaitUnitTask (bot.SendChatActionAsync(chatId, ChatAction.Typing))
do! timeOutMillis (String.length msg * 10 + 100)
do! Job.awaitUnitTask (bot.SendTextMessageAsync(chatId, msg))
}
toChatMessage = fun convo (msg: Message) ->
{
chatId = msg.Chat.Id
content = msg.Text
conversation = convo
internalMessage = msg
}
chatIdFromMessage = fun msg -> msg.Chat.Id
authorFromMessage = fun msg -> msg.From
getInitialHandler = fun () -> initialHandler
}
job {
let! botState = Bot.startNew impl
bot.OnMessage.Add (fun x -> Mailbox.send botState.serverMb (MessageReceived x.Message) |> start)
return botState
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment