Git Product home page Git Product logo

canoe's Introduction

canoe

Continuous Integration Gitter

Maven Central Telegram

Overview

canoe is a purely functional, compositional library for building interactive Telegram bots. It provides functional streaming interface over Telegram Bot API with built-in abstractions for describing your chatbot behavior.

Getting started

sbt dependency:

libraryDependencies += "org.augustjune" %% "canoe" % "<version>"

You can find the latest version in releases tab or by clicking on the maven-central badge. The library is available for Scala 2.12, 2.13, Scala 3 and Scala.js.

Imports:

import canoe.api._
import canoe.syntax._

The problem

Building interactive chatbots requires maintaining the state of each conversation, with possible interaction across them and/or using shared resources. The complexity of this task grows rapidly with the advancement of the bot. canoe solves this problem by decomposing behavior of the bot into a set of scenarios which the chatbot will follow.

Basic example

Here's a quick example of how the definition of simple bot behavior looks like in canoe. More samples can be found here.

import canoe.api._
import canoe.syntax._
import cats.effect.Async
import fs2.Stream

def app[F[_]: Async]: F[Unit] =
  Stream
    .resource(TelegramClient[F](token))
    .flatMap(implicit client => Bot.polling[F].follow(greetings))
    .compile
    .drain

def greetings[F[_]: TelegramClient]: Scenario[F, Unit] =
    for {
      chat <- Scenario.expect(command("hi").chat)
      _    <- Scenario.eval(chat.send("Hello. What's your name?"))
      name <- Scenario.expect(text)
      _    <- Scenario.eval(chat.send(s"Nice to meet you, $name"))
    } yield ()

Scenarios are executed concurrently in a non-blocking fashion, allowing to handle multiple users at the same time. In fact, even the same scenario can be triggered multiple times before the previous execution is completed. This can be extremely useful when you allow users to schedule long-running jobs and don't want to make them wait before they can schedule the new ones. As example may serve a simple alarm clock implementation.

Telegram Bot API methods

Low level abstractions are available through standalone Telegram Bot API methods from canoe.methods package. Having instance of TelegramClient in implicit scope, you can use call method on constructed action in order to execute it in effect F.

def sendText[F[_]: TelegramClient](chatId: Long, text: String): F[TextMessage] =
  SendMessage(chatId, text).call

As an alternative, all the methods from Telegram Bot API are available from corresponding models, e.g.chat.kickUser(user.id), message.editText("edited").

Webhook support

canoe also provides support for obtaining messages from Telegram by setting a webhook. Full example may be found here.

Handling errors

There's a lot of things that may go wrong during your scenarios executions, from user input to the network issues. For this reason, Scenario forms a MonadError for any F. It means that you can use built-in handleErrorWith and attempt methods, in order to react to the raised error or ensure that bot workflow won't break. Full example may be found here.

Contribution

If you're interested in the project PRs are very welcomed. In case it's a feature you'd like to introduce, it is recommended to discuss it first by raising an issue or simply using gitter.

canoe's People

Contributors

augustjune avatar eanea avatar igor-ramazanov avatar intfox avatar jesusmtnez avatar paharmonk avatar scala-steward avatar sunlightleaf avatar terjokhin avatar yyukan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

canoe's Issues

Only one scenario

From the README:

Scenarios are executed concurrently in a non-blocking fashion, allowing to handle multiple users at the same time. In fact, even the same scenario can be triggered multiple times before the previous execution is completed.

Can I forbid start of the new scenario if previous isn't completed?

SSLException

Hi, thanks for making this library!

I'm trying to get the simple Greetings example working with a bot I have created but I've observed an SSLException in the logs after many POST requests are attempted.

Have you seen this error before? I've never used the Telegram Bot API so maybe I'm doing something wrong?

17:47:55.348 [ioapp-compute-0] DEBUG org.http4s.client.blaze.Http1Connection - Beginning request: POST https://api.telegram.org/bot[MY_TOKEN]/getUpdates
17:47:55.348 [ioapp-compute-3] DEBUG org.http4s.blazecore.IdleTimeoutStage - Starting idle timeout stage with timeout of 60000 ms
17:47:56.410 [InnocuousThread-10] DEBUG org.http4s.blaze.pipeline.stages.SSLStage - SSL Read Request Status: Status = OK HandshakeStatus = NOT_HANDSHAKING
bytesConsumed = 521 bytesProduced = 492, java.nio.HeapByteBuffer[pos=492 lim=16921 cap=16921]
17:47:56.410 [InnocuousThread-10] DEBUG org.http4s.blaze.pipeline.stages.SSLStage - SSL Read Request Status: Status = BUFFER_UNDERFLOW HandshakeStatus = NOT_HANDSHAKING
bytesConsumed = 0 bytesProduced = 0, java.nio.HeapByteBuffer[pos=0 lim=16921 cap=16921]
17:47:56.410 [InnocuousThread-10] DEBUG org.http4s.blaze.pipeline.stages.SSLStage - SSL Read Request Status: Status = BUFFER_UNDERFLOW HandshakeStatus = NOT_HANDSHAKING
bytesConsumed = 0 bytesProduced = 0, java.nio.HeapByteBuffer[pos=0 lim=16921 cap=16921]
17:47:56.412 [scala-execution-context-global-225] DEBUG org.http4s.client.blaze.Http1Connection - Resetting org.http4s.client.blaze.Http1Connection after completing request.
17:47:56.420 [scala-execution-context-global-225] DEBUG org.http4s.blazecore.IdleTimeoutStage - Shutting down idle timeout stage
17:47:56.420 [scala-execution-context-global-225] DEBUG org.http4s.blazecore.IdleTimeoutStage - Shutting down.
17:47:56.420 [scala-execution-context-global-225] DEBUG org.http4s.blazecore.IdleTimeoutStage - Removed mid-stage: IdleTimeoutStage
17:47:56.421 [scala-execution-context-global-225] DEBUG org.http4s.client.blaze.ReadBufferStage - Shutting down.
17:47:56.429 [scala-execution-context-global-225] DEBUG org.http4s.blaze.pipeline.stages.SSLStage - Error while closing SSL Engine
javax.net.ssl.SSLException: Inbound closed before receiving peer's close_notify: possible truncation attack?
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
	at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1647)
	at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1615)
	at sun.security.ssl.SSLEngineImpl.closeInbound(SSLEngineImpl.java:1542)
	at org.http4s.blaze.pipeline.stages.SSLStage.stageShutdown(SSLStage.scala:61)
	at org.http4s.blaze.pipeline.Tail.closePipeline(Stages.scala:78)
	at org.http4s.blaze.pipeline.Tail.closePipeline$(Stages.scala:77)
	at org.http4s.blaze.pipeline.stages.SSLStage.closePipeline(SSLStage.scala:41)
	at org.http4s.blaze.pipeline.Tail.closePipeline(Stages.scala:81)
	at org.http4s.blaze.pipeline.Tail.closePipeline$(Stages.scala:77)
	at org.http4s.client.blaze.ReadBufferStage.closePipeline(ReadBufferStage.scala:13)
	at org.http4s.blaze.pipeline.Tail.closePipeline(Stages.scala:81)
	at org.http4s.blaze.pipeline.Tail.closePipeline$(Stages.scala:77)
	at org.http4s.client.blaze.Http1Connection.closePipeline(Http1Connection.scala:25)
	at org.http4s.client.blaze.Http1Connection.shutdownWithError(Http1Connection.scala:83)
	at org.http4s.client.blaze.Http1Connection.stageShutdown(Http1Connection.scala:54)
	at org.http4s.client.blaze.Http1Connection.shutdown(Http1Connection.scala:52)
	at org.http4s.client.PoolManager.$anonfun$invalidate$1(PoolManager.scala:313)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:87)
	at cats.effect.internals.IORunLoop$.startCancelable(IORunLoop.scala:41)
	at cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:86)
	at cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:70)
	at cats.effect.internals.Trampoline.startLoop(Trampoline.scala:36)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:93)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:93)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:93)
	at cats.effect.internals.Trampoline.execute(Trampoline.scala:43)
	at cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:44)
	at cats.effect.internals.Callback$AsyncIdempotentCallback.apply(Callback.scala:137)
	at cats.effect.internals.Callback$AsyncIdempotentCallback.apply(Callback.scala:124)
	at org.http4s.client.blaze.Http1Connection.parsePrelude(Http1Connection.scala:296)
	at org.http4s.client.blaze.Http1Connection.$anonfun$readAndParsePrelude$1(Http1Connection.scala:189)
	at org.http4s.client.blaze.Http1Connection.$anonfun$readAndParsePrelude$1$adapted(Http1Connection.scala:188)
	at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:447)
	at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
17:47:56.431 [scala-execution-context-global-225] DEBUG org.http4s.blaze.channel.nio2.ByteBufferHead - Shutting down.
17:47:56.432 [scala-execution-context-global-225] DEBUG org.http4s.blaze.channel.nio2.ByteBufferHead - doClosePipeline(None)
17:47:56.432 [scala-execution-context-global-225] DEBUG org.http4s.client.blaze.Http1Connection - Shutting down.
17:47:56.435 [scala-execution-context-global-225] DEBUG org.http4s.client.PoolManager - Invalidated connection, no pending requests. Shrinking pool: curAllocated=0 idleQueues.size=0 waitQueue.size=0 maxWaitQueueLimit=256 closed=false
17:47:56.471 [ioapp-compute-0] INFO org.http4s.client.PoolManager - Shutting down connection pool: curAllocated=0 idleQueues.size=0 waitQueue.size=0 maxWaitQueueLimit=256 closed=false
org.http4s.client.UnexpectedStatus: unexpected HTTP status: 409 Conflict

Exception: sbt.TrapExitSecurityException thrown from the UncaughtExceptionHandler in thread "run-main-0"

Update scenario with user callback response.

Suppose we have the following example:

package samples

import canoe.api._
import canoe.api.models.Keyboard
import canoe.models.{CallbackButtonSelected, InlineKeyboardButton, InlineKeyboardMarkup, Update}
import canoe.models.messages.{AnimationMessage, StickerMessage, TelegramMessage, TextMessage}
import canoe.syntax._
import cats.{Applicative, Monad}
import cats.effect.{IO, IOApp}
import cats.syntax.all._
import fs2.{Pipe, Stream}

/** Example of echos bot that will answer to you with
  * the callback data [[canoe.models.InlineKeyboardButton.callbackData]]
  * has been sent to him
  */
object CallbackHandling extends IOApp.Simple {
  val token: String = "<your telegram token>"

  def run: IO[Unit] =
    Stream
      .resource(TelegramClient.global[IO](token))
      .flatMap { implicit client =>
        Bot
          .polling[IO]
          .follow(echos)
          .through(answerCallbacks)
      }
      .compile
      .drain

  val inlineBtn = InlineKeyboardButton.callbackData(text = "Pick color", cbd = "callback data")

  val greenBtn = InlineKeyboardButton.callbackData(text = "Green", "g")
  val redBtn = InlineKeyboardButton.callbackData(text = "Red", "r")
  val inlineKeyboardMarkUp = InlineKeyboardMarkup.singleRow(Seq(greenBtn, redBtn))
  val keyboardMarkUp = Keyboard.Inline(inlineKeyboardMarkUp)

  def echos[F[_]: TelegramClient]: Scenario[F, Unit] =
    for {
      msg <- Scenario.expect(command("callback"))
      _   <- Scenario.eval(msg.chat.send(content = "pretty message", keyboard = keyboardMarkUp))
      color <- Scenario.expect(callbackResponse)
      _   <- Scenario.eval(msg.chat.send("You pick "+color+" color"))
    } yield ()

  def answerCallbacks[F[_]: Monad: TelegramClient]: Pipe[F, Update, Update] =
    _.evalTap {
      case CallbackButtonSelected(_, query) =>
        query.data match {
          case Some(cbd) =>
            for {
              _ <- query.message.traverse(_.chat.send(cbd))
              _ <- query.finish
            } yield ()
          case _ => Applicative[F].unit
        }
      case _ => Applicative[F].unit
    }
}

Would it be possible to run from some implementation that can work as expected to do "callbackResponse"?

Default error handlers

Once any IO inside Scenario.eval() fails with some exception an application halts.
Can we implement a way of handling such exceptions inside scenarios? For example it would be useful to convert such handled exceptions into human-readable message.

Pin message in Private chat

Please, remove restriction when Bot can't pin a message in private(one-to-one) chats. As I know , right now private chats are equated to supergroups thats why default tg API allows to pin messages.

Add an ability to expect callback query

Hello, guys. I see among the open issues a topic with the possibility of waiting for a message to be entered after receiving a callback. I will try to explain it in more details. What is the main problem? Sometimes I need to expect input message from the user after he has selected one of the options in the inline keyboard. It means that firstly I need to expect CallbackButtonSelected update and then MessageReceived update in one Scenario. Right now I can't do it. I'll give you an example from a bot I'm working on right now. I want to create a bot which will help students to take place in a queue for various exams(like queue for doctor but for teacher in university). I have a command "Take place". What is the flow? - 1. select a teacher from the list 2. enter the date 3. choose a place from the list of available places. Flow looks like CallbackButtonSelected -> MessageReceived -> CallbackButtonSelected. Right now your API allows build flows only with MessageReceived updates. It is not suitable! I need to store intermediate data(for example date about previous selected buttons) in CallbackData to pass it from one callback handler to another (or use other approaches such as,for example, Redis). The second problem is that I need to wait until the button selected in most cases and it means suspend scenario flow.
In my project I slightly modified your library to have an ability to build such flows. I think it would be nice to add feature to expect callback answers in scenarios.
if you think my idea is correct, then I can share a little more details in this topic.
Thanks in advance

Functor syntax and so on

It would be nice to use the syntax given by cats.implicits._ when working with Scenario[F, A].

I see that it's defined as:

type Scenario[F[_], A] = Episode[F, TelegramMessage, A]

And that Episode defines a Monad instance but it might need something else to pick up the syntax. For example:

def repeat[F[_]: TelegramClient: Timer](chat: Chat, i: Int): Scenario[F, Unit] =
  if (i <= 0) Scenario.eval(chat.send("Done.")).map(_ => ())
  else
    for {
      _ <- Scenario.eval(chat.send(s"$i.."))
      _ <- Scenario.eval(Timer[F].sleep(1.second))
      _ <- repeat(chat, i - 1)
    } yield ()

I would like to be able to do:

Scenario.eval(chat.send("Done.")).void

But that's just an example.

Improve package structure

User should be able to use the library with a minimum number of imports.
Each import statement should introduce an extra piece of API,

Issues sending an album

When sending an album with

Scenario.eval(chat.sendAlbum(List(
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/j24nKTO.png")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/nrMrWv8.jpg")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/ihme12x.jpg")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/mcwaUgv.jpg")),
      )))

I get org.http4s.client.UnexpectedStatus: unexpected HTTP status: 400 Bad Request

However, this works

Scenario.eval(chat.send(PhotoContent(InputFile.fromUrl("https://i.imgur.com/mcwaUgv.jpg"), "is this real life?")))

This is the entire function

  def start[F[_]: TelegramClient]: Scenario[F, Unit] =
    for {
      chat          <- Scenario.expect(command("share").chat)
      _             <- Scenario.eval(chat.send(s"hello right back at you!"))
      // _ <- Scenario.eval(chat.send(PhotoContent(InputFile.fromUrl("https://i.imgur.com/mcwaUgv.jpg"), "is this real life?")))
      _             <- Scenario.eval(chat.sendAlbum(List(
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/j24nKTO.png")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/nrMrWv8.jpg")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/ihme12x.jpg")),
        InputMediaPhoto(InputFile.fromUrl("https://i.imgur.com/mcwaUgv.jpg")),
      )))
    } yield ()

I'm new to Scala and to FP, but I'd be happy to provide more details. I'm not certain whether this is a bug or if it's an error on my part.

JSON Decoding Error

Hey, thanks for creating this; I am playing around a little with canoe and encountered a bug: Whenever I poll for Updates with my bot (using Bot.polling[F].update), the application crashes with the following message/stack trace:

org.http4s.InvalidMessageBodyFailure: Invalid message body: Could not decode JSON: {
"ok" : true,
"result" : [
{
"update_id" : 112088157,
"message" : {
"message_id" : 479,
"from" : {
"id" : ,
"is_bot" : false,
"first_name" : "Jan",
"language_code" : "de"
},
"chat" : {
"id" : -357322183,
"title" : "Schnicktest1",
"type" : "group",
"all_members_are_administrators" : true
},
"date" : 1569522010,
"group_chat_created" : true
}
},
{
"update_id" : 112088158,
"message" : {
"message_id" : 480,
"from" : {
"id" : ,
"is_bot" : false,
"first_name" : "Jan",
"language_code" : "de"
},
"chat" : {
"id" : -357322183,
"title" : "Schnicktest1",
"type" : "group",
"all_members_are_administrators" : true
},
"date" : 1569522015,
"text" : "/schnick",
"entities" : [
{
"offset" : 0,
"length" : 8,
"type" : "bot_command"
}
]
}
},
{
"update_id" : 112088159,
"message" : {
"message_id" : 481,
"from" : {
"id" : ,
"is_bot" : false,
"first_name" : "Jan",
"language_code" : "de"
},
"chat" : {
"id" : -357322183,
"title" : "Schnicktest1",
"type" : "group",
"all_members_are_administrators" : true
},
"date" : 1569522021,
"text" : "/help",
"entities" : [
{
"offset" : 0,
"length" : 5,
"type" : "bot_command"
}
]
}
},
{
"update_id" : 112088160,
"message" : {
"message_id" : 482,
"from" : {
"id" : ,
"is_bot" : false,
"first_name" : "Jan",
"language_code" : "de"
},
"chat" : {
"id" : -357322183,
"title" : "Schnicktest1",
"type" : "group",
"all_members_are_administrators" : true
},
"date" : 1569522022,
"text" : "/blarn",
"entities" : [
{
"offset" : 0,
"length" : 6,
"type" : "bot_command"
}
]
}
}
]
}
at org.http4s.circe.CirceInstances$.$anonfun$defaultJsonDecodeError$1(CirceInstances.scala:189)
at org.http4s.circe.CirceInstances.$anonfun$jsonOfWithMedia$2(CirceInstances.scala:71)
at scala.util.Either.fold(Either.scala:192)
at org.http4s.circe.CirceInstances.$anonfun$jsonOfWithMedia$1(CirceInstances.scala:72)
at cats.data.EitherT.$anonfun$flatMap$1(EitherT.scala:100)
at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:139)
at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:376)
at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:316)
at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:136)
at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
at cats.effect.internals.IORunLoop$RestartCallback.run(IORunLoop.scala:366)
at cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:70)
at cats.effect.internals.Trampoline.startLoop(Trampoline.scala:36)
at cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:93)
at cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:93)
at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
at cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:93)
at cats.effect.internals.Trampoline.execute(Trampoline.scala:43)
at cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:44)
at cats.effect.internals.Callback$AsyncIdempotentCallback.apply(Callback.scala:137)
at cats.effect.internals.Callback$AsyncIdempotentCallback.apply(Callback.scala:124)
at org.http4s.client.blaze.Http1Connection.parsePrelude(Http1Connection.scala:296)
at org.http4s.client.blaze.Http1Connection.$anonfun$readAndParsePrelude$1(Http1Connection.scala:189)
at org.http4s.client.blaze.Http1Connection.$anonfun$readAndParsePrelude$1$adapted(Http1Connection.scala:188)
at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:447)
at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: DecodingFailure(Attempt to decode value on failed cursor, List(DownField(preCheckoutQuery), DownArray, DownField(result)))

Exception: sbt.TrapExitSecurityException thrown from the UncaughtExceptionHandler in thread "run-main-0"`

(You might need to copy+paste the JSON into a formatter, sorry about that... And I removed the user ID of the messages, in the original JSON they are 8-digit numbers.)

These messages have not been handled/deleted yet and thus I cannot use my bot at all at the moment (with canoe) since they immediately come up when polling.

I guess there's something wrong with a Decoder which seems odd to me since (as far as I could tell) they are all auto/semiauto-derived, aren't they?

I cloned your repo and will try to find the problem / fix it and open a PR, but I wanted to let you know ASAP.

Encapsulate Episode algebra

At the moment Scenario type is just an alias to the Episode with a fixed input type. The separation of these two concepts, with Scenario being user API of Episode, will allow having simpler Scenario methods definitions and block a user from unwanted transition to the Episode type.

Guarantee Scenario stack safety during the interpretation

Deeply stacked Scenario instance causes stack overflow during the interpretation.

def stack[F[_]](n: Long): Scenario[F, Long] = {
  def bind(n: Long, sc: Scenario[F, Long]): Scenario[F, Long] =
    if (n <= 0) sc
    else bind(n - 1, sc.flatMap(l => Scenario.pure(l + 1)))
  
  bind(n, Scenario.pure(0))
}

Stream.empty.through(stack[IO](10000).pipe).compile.drain.unsafeRunSync()
// Exception in thread "main" java.lang.StackOverflowError ...

Support timeouts for Scenario

By supporting timeouts library allows to write time-dependent scenarios (sic!), i.e. quizzes, captchas and to not waste server resources on stale scenarios.
Suggested method is

Scenario {
...
def withTimeout[B](timeout: TimeDuration)(sfb: Scenario[F, B])
...
}

And it could be used like

Scenario.eval(sendCaptcha).withTimeout(5.seconds)(Scenario.eval(kickUser))

Could be implemented as a race between current effect and passed one

Support no notification option for answerCallbackQuery

According to CallbackQuery docs

NOTE: After the user presses a callback button, Telegram clients will display a progress bar until you call answerCallbackQuery. It is, therefore, necessary to react by calling answerCallbackQuery even if no notification to the user is needed (e.g., without specifying any of the optional parameters).

Add an ability to download files sent by user

Hello, guys. Maybe I missed something but i can't find any methods to download files (for example which I get from DocumentMessage update). But default Tg API has endpoint to do this (https://api.telegram.org/file/bot/<file_path>). You also mentioned this endpoint in your scaladoc comments for method GetFile. I think it would be nice to have a method to easily load data from chat

A way of cancelling scenarios from inside an app

For now there are methods like scenario.cancelOn(...) allowing to cancel scenarios on some user input. But in many cases we need to cancel scenario on some internal event, for example if some service our bot interacts with is unavailiable.
Is it possible to implement such a method?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.