Skip to content

Repository files navigation

Idiomatic Scala wrapper for the Telegram Bot API

bot4s.telegram

Simple, extensible, strongly-typed wrapper for the Telegram Bot API.

Table of contents

Installation

Since version 6.0.0 telegram-core and telegram-pekko are published for Scala 2.12, 2.13 and 3.

Add to your build.sbt file:

// Core with minimal dependencies, enough to spawn your first bot.
libraryDependencies += "com.bot4s" %% "telegram-core" % "7.1.0"

// Extra goodies: Webhooks, support for games, bindings for actors.
libraryDependencies += "com.bot4s" %% "telegram-pekko" % "7.1.0"

For Mill, add the dependencies to your build.mill module:

def mvnDeps = Seq(
  // Core with minimal dependencies, enough to spawn your first bot.
  mvn"com.bot4s::telegram-core:7.1.0",
  // Extra goodies: Webhooks, support for games, bindings for actors.
  mvn"com.bot4s::telegram-pekko:7.1.0"
)

Since version 7.1.0, telegram-core is also published for Scala.js on Scala 2.12, 2.13 and 3. In an sbt Scala.js project, use %%% to select the Scala.js artifact:

libraryDependencies += "com.bot4s" %%% "telegram-core" % "7.1.0"

In a Mill Scala.js module, use mvn"com.bot4s::telegram-core::7.1.0". telegram-pekko is JVM-only.

Quickstart with scala-cli.

Replace BOT_TOKEN with your Telegram bot token.

//> using scala 3.3.8
//> using dep "com.bot4s::telegram-core:7.1.0"
//> using dep "com.softwaremill.sttp.client4::okhttp-backend:4.0.27"
//
import cats.syntax.functor.*
import scala.concurrent.*
import scala.concurrent.duration.*
import com.bot4s.telegram.api.RequestHandler
import com.bot4s.telegram.clients.FutureSttpClient
import com.bot4s.telegram.future.*
import com.bot4s.telegram.methods.SendMessage
import com.bot4s.telegram.models.Message
import sttp.client4.Backend
import sttp.client4.okhttp.OkHttpFutureBackend

/**
 * Echo bot.
 * Echo, ohcE
 */
class EchoBot(token: String) extends TelegramBot with Polling {
  implicit val backend: Backend[Future] = OkHttpFutureBackend()
  override val client: RequestHandler[Future]    = new FutureSttpClient(token)

  override def receiveMessage(msg: Message): Future[Unit] =
    msg.text.fold(Future.successful(())) { text =>
      request(SendMessage(msg.source, text.reverse)).void
    }
}

@main def main() = {
  // To run spawn the bot
  val bot = new EchoBot("BOT_TOKEN")
  val eol: Future[Unit] = bot.run()
  println("Press [ENTER] to shutdown the bot, it may take a few seconds...")
  scala.io.StdIn.readLine()
  bot.shutdown() // initiate shutdown
  // Wait for the bot end-of-life
  Await.result(eol, Duration.Inf)
}

Examples

Random bot (full example)

class RandomBot(token: String) extends ExampleBot(token) with Polling with Commands[Future] {

  val rng = new scala.util.Random(System.currentTimeMillis())
  onCommand("coin" or "flip") { implicit msg =>
    reply(if (rng.nextBoolean()) "Head!" else "Tail!").void
  }
  onCommand("real" | "double" | "float") { implicit msg =>
    reply(rng.nextDouble().toString).void
  }
  onCommand("/die" | "roll") { implicit msg =>
    reply("⚀⚁⚂⚃⚄⚅" (rng.nextInt(6)).toString).void
  }
  onCommand("random" or "rnd") { implicit msg =>
    withArgs {
      case Seq(Int(n)) if n > 0 =>
        reply(rng.nextInt(n).toString).void
      case _ => reply("Invalid argumentヽ(ಠ_ಠ)ノ").void
    }
  }
  onCommand("choose" | "pick" | "select") { implicit msg =>
    withArgs { args =>
      replyMd(if (args.isEmpty) "No arguments provided." else args(rng.nextInt(args.size))).void
    }
  }

  onCommand("auto") { implicit msg =>
    request(SendDice(msg.chat.id)).void
  }
  // Extractor
  object Int {
    def unapply(s: String): Option[Int] = Try(s.toInt).toOption
  }

}

Text to speech bot (full example)

/**
 * Text-to-speech bot (using Google TTS API)
 *
 * Google will rightfully block your IP in case of abuse.
 * '''Usage:''' /speak Hello World
 * '''Inline mode:''' @YourBot This is awesome
 */
class TextToSpeechBot(token: String)
    extends ExampleBot(token)
    with Polling
    with Commands[Future]
    with InlineQueries[Future]
    with ChatActions[Future] {

  def ttsUrl(text: String): String =
    s"http://translate.google.com/translate_tts?client=tw-ob&tl=en-us&q=${URLEncoder.encode(text, "UTF-8")}"

  onCommand("speak" | "say" | "talk") { implicit msg =>
    withArgs { args =>
      val text = args.mkString(" ")
      for {
        r <- Future(scalaj.http.Http(ttsUrl(text)).asBytes)
        if r.isSuccess
        bytes    = r.body
        _       <- uploadingAudio // hint the user
        voiceMp3 = InputFile("voice.mp3", bytes)
        _       <- request(SendVoice(msg.source, voiceMp3))
      } yield ()
    }
  }

  def nonEmptyQuery(iq: InlineQuery): Boolean = iq.query.nonEmpty

  whenOrElse(onInlineQuery, nonEmptyQuery) { implicit iq =>
    answerInlineQuery(
      Seq(
        // Inline "playable" preview
        InlineQueryResultVoice("inline: " + iq.query, ttsUrl(iq.query), iq.query),
        // Redirection to /speak command
        InlineQueryResultArticle(
          "command: " + iq.query,
          iq.query,
          inputMessageContent = InputTextMessageContent("/speak " + iq.query),
          description = "/speak " + iq.query
        )
      )
    ).void
  } /* empty query */ {
    answerInlineQuery(Seq())(_).void
  }
}

Calculator bot (using Webhooks) (full example)

class WebhookBot(token: String) extends PekkoExampleBot(token) with Webhook {
  val port       = 8080
  val webhookUrl = "https://88c444ab.ngrok.io"

  val baseUrl = "http://api.mathjs.org/v1/?expr="

  override def receiveMessage(msg: Message): Future[Unit] =
    msg.text.fold(Future.successful(())) { text =>
      val url = baseUrl + URLEncoder.encode(text, "UTF-8")
      for {
        res <- Http().singleRequest(HttpRequest(uri = Uri(url)))
        if res.status.isSuccess()
        result <- Unmarshal(res).to[String]
        _      <- request(SendMessage(msg.source, result))
      } yield ()
    }
}

Check out the sample bots for more functionality.

Leaking bot tokens

Don't ever expose your bot's token.

Hopefully GitGuardian got you covered and will warn you about exposed API keys.

Webhooks vs. Polling

Both methods are supported. (Long) Polling is bundled in the core artifact and it's by far the easiest method.

Webhook support comes in the extra artifact based on pekko-http; requires a server, it won't work on your laptop. For a comprehensive reference check Marvin's Patent Pending Guide to All Things Webhook.

Some webhook examples are available here and here (with self signed SSL certificate setup).

Payments

Payments are supported since version 3.0; refer to official payments documentation for details. I'll support developers willing to integrate and/or improve the payments API; please report issues here.

Games

The Pekko extensions include support for games in two flavors; self-hosted (served by the bot itself), and external, hosted on e.g. GitHub Pages. Check both the self-hosted and GitHub-hosted versions of the popular 2048 game.

Deployment

bot4s.telegram runs on Raspberry Pi, Heroku, Google App Engine and most notably on an old Android (4.1.2) phone with a broken screen via the JDK for ARM. Bots also runs flawlessly on top of my master thesis: "A meta-circular Java bytecode interpreter for the GraalVM".

Distribution/deployment is outside the scope of the library, but all platforms where Java is supported should be compatible. You may find sbt-assembly and sbt-docker very handy.

Scala.js is also supported via the SttpClient and FetchBackend, including Node.js (see below).

Running the examples

bot4s.telegram uses Mill.

./mill examples.jvm.2_13_18.console
Welcome to Scala 2.13.18.
Type in expressions for evaluation. Or try :help.

scala> new RandomBot("BOT_TOKEN").run()

Change RandomBot to whatever bot you find interesting here.

Node.js

With Node.js 22 or newer on your PATH, run from the repository root:

 export BOT_TOKEN='YOUR_BOT_TOKEN'
./mill --no-daemon examples.js.2_13_18.run

This launches EchoBot, which replies with your message reversed. To run RandomBot instead:

BOT_NAME=RandomBot ./mill --no-daemon examples.js.2_13_18.run

Send /coin to try RandomBot. Stop either bot with Ctrl+C.

Use --no-daemon to avoid Mill 1.1.7's Scala.js stdin forwarding failure (missing keys in dictionary: bytes) in daemon mode.

Both platforms compile the same EchoBot.scala, RandomBot.scala, and ExampleBot.scala sources. Only the HTTP backend differs: OkHttp on JVM and FetchBackend on JS, using Node's built-in Fetch API. The JS target currently includes these two examples; other examples may require JVM-only APIs.

A note on implicits

A few implicits are provided to reduce boilerplate, but are discouraged because unexpected side-effects.

Think seamless T => Option[T] conversion, Markdown string extensions (these are fine)... Be aware that, for conciseness, most examples need the implicits to compile, be sure to include them.

import com.bot4s.telegram.Implicits._

Versioning

This library uses Semantic Versioning. For the versions available, see the tags on this repository.

Authors

  • Alfonso² Peterssen - Owner/maintainer - :octocat: mukel

Looking for maintainers!

See also the list of awesome contributors who participated in this project. Contributions are very welcome, documentation improvements/corrections, bug reports, even feature requests.

License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.

Buy Me A Coffee

Buy Me A Coffee

If you like this library, please consider buying me a coffee. ☺️

Releases

Packages

Used by

Contributors

Languages