DEV Community

loading...
The DEV Team

DEV ❤️ Telegram

citizen428 profile image Michael Kohl Updated on ・3 min read

Here at DEV we roughly follow the Shape Up product development methodology. This includes regular downtimes and during the current one, I built a Telegram bot as a fun little side project.

Meet @devto_bot

Our bot's first feature has been implemented inline, similar to how the IMDB or Wikipedia bots work.

You can find it at @devto_bot, but you don't really need to do that since it's automatically available in all your private and group chats. Don't worry, our bot runs in privacy mode so it won't be reading your messages.

To use it simply mention @devto_bot, optionally followed by a tag name. This will bring up a list of the 5 most recent articles in that tag. While this is not quite as helpful as I'd like, our API doesn't currently support article search. Once we've added that I'll update the bot accordingly.

Let's see it in action:
bot video

The implementation

The bot is written in Crystal, using the Tourmaline Telegram bot API framework.

This is the main part of the code:

require "tourmaline"
require "./api_client"

class DevtoBot < Tourmaline::Client
  VERSION = "0.1.0"

  @[On(:inline_query)]
  def on_inline_query(client, update)
    tag = update.inline_query.try(&.query)
    articles = ApiClient.articles(tag)

    results = build_results(articles)
    update.inline_query.try(&.answer(results))
  end

  private def build_results(articles)
    InlineQueryResult.build do |builder|
      articles.map do |article|
        builder.article(
          id: article.id.to_s,
          title: article.title,
          thumb_url: article.social_image,
          input_message_content: InputTextMessageContent.new(article.url),
          description: article.description
        )
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

We receive the tag via an inline query and use it in the request to the DEV API. We then use Tourmaline::InlineQueryResult::Builder to turn the resulting objects into query results for the bot's popup.

The API client uses HTTP::Client from Crystal's standard library and is very minimal:

require "http"
require "./article"

class ApiClient
  API_URL = "https://dev.to/api/%s?%s"

  def self.articles(tag)
    query = HTTP::Params.encode({per_page: "5", tag: tag})
    url = sprintf API_URL, "/articles", query
    response = HTTP::Client.get(url)
    Article.array_from_json(response.body)
  end
end
Enter fullscreen mode Exit fullscreen mode

The Article class defines a mapping between JSON attributes and a Crystal object.

require "json"

class Article
  # Only map the few attributes we need
  JSON.mapping({
    id:           {type: Int32},
    title:        {type: String},
    url:          {type: String},
    social_image: {type: String?},
    description:  {type: String?},
  })

  def self.array_from_json(json)
    Array(self).from_json(json)
  end
end
Enter fullscreen mode Exit fullscreen mode

The bot is hosted on Heroku with the Crystal buildpack. We use the following app.cr and Procfile:

require "env"

require "./src/devto_bot"

token = ENV.fetch("TELEGRAM_BOT_TOKEN")
bot = DevtoBot.new(token)
bot.set_webhook("https://.../bot-webhook/#{token}")
bot.serve("0.0.0.0", ENV.fetch("PORT").to_i)
Enter fullscreen mode Exit fullscreen mode
web: ./app --port $PORT
Enter fullscreen mode Exit fullscreen mode

Once I clean up the code a bit and add some basic documentation I'll make the repository public.

Your turn

How about you, do you use Telegram? Do you have ideas for other features you'd like to see from this bot? Or is there some other platform you would really like to see a DEV bot for? Let us know in the comments!

Discussion (16)

pic
Editor guide
Collapse
sergix profile image
Peyton McGinnis

I actually just started messing around last week building a small server with Crystal. Really quite a charm, and very fast too.

Collapse
citizen428 profile image
Michael Kohl Author • Edited

Crystal actually sits at a pretty sweet spot: the expressiveness and fun of Ruby, better speed, macros for metaprogramming, Go-like async. Also, some cool web frameworks emerging, like Lucky (my favorite to which I occasionally contribute) or Amber. I'd really like it to succeed, but with the attention and support other languages get from big companies, it's hard to carve out a niche.

Collapse
sergix profile image
Peyton McGinnis

Yeah, it took me a while to find some answers on a couple implementation issues I was having since it's still a relatively unknown language. It'll gain more adoption once it finally reaches 1.0.0 though, so I'm just waiting for that.

Thread Thread
citizen428 profile image
Michael Kohl Author

There's is a PragProg book out since last year:

pragprog.com/book/crystal/programm...

I didn't exactly find it mindblowing, but it's a solid intro and I decided to purchase it to show support for the language.

Thread Thread
sergix profile image
Peyton McGinnis

Oh, awesome! I'll definitely consider purchasing it. Thanks for letting me know!

Collapse
moagggi profile image
moagggi • Edited

Awesome and nice bot as well as an informative article! :)

I've noticed one thing: doesn't
url = sprintf API_URL, "/articles", query
with
API_URL = "https://dev.to/api/%s?%s"
result in
https://dev.to/api//articles?per_page=...&tag=...
with a double slash before articles?
This could lead to malfunction/failure as it seems unintentionally to me.

Collapse
citizen428 profile image
Michael Kohl Author

Good catch, like I said the code still needs some cleaning up. It works ok, but is indeed unintentional.

Collapse
watzon profile image
Chris Watson

I'd actually recommend against using sprintf and just use string interpolation instead, though in this case since you're dealing with a URL File.join might actually be the better way to go.

Thread Thread
citizen428 profile image
Michael Kohl Author

This was built in about 4-5h while learning the Telegram bot API on the side. There are a few more things I’ll eventually clean up, especially when adding more commands.

Collapse
ishan0445 profile image
Ishan Rayeen

A few days ago I created a video tutorial on this topic, Even I used the dev.to API to teach how to implement inline queries in a telegram bot,
dev.to/ishan0445/handling-inline-q...

Collapse
watzon profile image
Chris Watson

Thanks for using Tourmaline and giving it a little more exposure with this article!

Collapse
citizen428 profile image
Michael Kohl Author

Thanks for making a great library! 😊

Collapse
cescquintero profile image
Francisco Quintero 🇨🇴

Is this your first time writing something in Crystal Lang? How does it feel compared to Ruby?

Collapse
citizen428 profile image
Michael Kohl Author

Is this your first time writing something in Crystal Lang?

No, I've been interested in Crystal since 2016 and also used to sponsor the langugae via BountySource for a while. I have a couple of small Shards and contributed to the Lucky web framework.

How does it feel compared to Ruby?

I wrote two articles about that:

Collapse
Sloan, the sloth mascot
Comment deleted