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.
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
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
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
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)
web: ./app --port $PORT
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
I actually just started messing around last week building a small server with Crystal. Really quite a charm, and very fast too.
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.
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.
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.
Oh, awesome! I'll definitely consider purchasing it. Thanks for letting me know!
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.
Good catch, like I said the code still needs some cleaning up. It works ok, but is indeed unintentional.
I'd actually recommend against using
sprintf
and just use string interpolation instead, though in this case since you're dealing with a URLFile.join
might actually be the better way to go.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.
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...
Thanks for using Tourmaline and giving it a little more exposure with this article!
Thanks for making a great library! 😊
Is this your first time writing something in Crystal Lang? How does it feel compared to Ruby?
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.
I wrote two articles about that:
A Rubyist looks at Crystal (Part 1)
Michael Kohl ・ Oct 30 '16 ・ 6 min read
A Rubyist looks at Crystal (Part 2)
Michael Kohl ・ Nov 20 '16 ・ 6 min read