Add part one of Songlify bot #15
|
@ -9,3 +9,4 @@ hugo.darwin
|
|||
hugo.linux
|
||||
|
||||
node_modules/
|
||||
.hugo_build.lock
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
---
|
||||
title: "Building a Telegram bot in Rust: a journey through Songlify"
|
||||
description: An adventure through Rust and Telegram
|
||||
tags:
|
||||
- blog
|
||||
- tech
|
||||
- rust
|
||||
- telegram
|
||||
- songlify
|
||||
date: 2022-01-05T17:01:00+02:00
|
||||
---
|
||||
|
||||
Some time passed since the last article I wrote there. A lot of stuff happened
|
||||
meanwhile, especially with COVID, but here we are again. While busy dealing with
|
||||
the mess of real life tasks, three months ago I started to write a little bot
|
||||
for Telegram in Rust. It is a simple one, but I consider the journey interesting
|
||||
and worth of writing it down. If I add new features worth mentioning I will
|
||||
start a series about it, maybe.
|
||||
|
||||
## Telegram bots
|
||||
|
||||
Telegram bots are not something new to me and nowadays are pretty much easy to
|
||||
make, so I consider them like a gym where to try out new technologies and
|
||||
experiment with stuff. I wrote plentiful of them, some of those are open source
|
||||
like for example <https://github.com/Augugrumi/TorreArchimedeBot> (which is
|
||||
currently broken 😭) that was useful when going to University, because it
|
||||
scraped the university free room web page and from there it was able to tell you
|
||||
which rooms where without any lessons and for how much time, allowing you to
|
||||
easily find a place where to study with your mates (yep, we didn't like library
|
||||
too much).
|
||||
|
||||
{{< figure src="/content/songlify/telegramscreen.png" alt=`A screenshot of
|
||||
TorreArchimedeBot in action.` caption=`A screenshot of TorreArchimedeBot in
|
||||
action.` >}}
|
||||
|
||||
Also another one bot worthy of mention is
|
||||
<https://github.com/Polpetta/RedditToTelegram>, that allowed our D&D group to
|
||||
receive push notifications of our private Subreddit in our Telegram group.
|
||||
|
||||
As you can see, all of these bots are quite simple, but they have the added
|
||||
value of teaching you some new programming concepts, technologies or frameworks
|
||||
that can be later applied in something that can be more production environment.
|
||||
|
||||
## Rust
|
||||
|
||||
I started to approach Rust many years ago (I do not remember exactly when).
|
||||
First interaction with it was quite interesting to say at least: there were way
|
||||
less compiler features (for example now the compiler is able to understand
|
||||
object lifetime at compile time most of the time alone, without specifying them)
|
||||
that made it a... _not-so-pleasant programming experience_. It had potential
|
||||
thought, so by following Rust news I picked it up last year again, noticing that
|
||||
now it has improved a lot and it is more pleasant to write. Meanwhile, also
|
||||
JetBrains developed a good support for IntelliJ, so now it is even possible to
|
||||
debug and perform every operation directly from your IDE UI.
|
||||
|
||||
## Making the two worlds collide: Rust + Telegram
|
||||
|
||||
One of the features I wanted to learn this time regarding Rust was the
|
||||
asynchronous support it offers. Rust started to have `async` support with
|
||||
[Tokio](https://tokio.rs/) framework, and recently the Rust team started to
|
||||
build the asynchronous functionality inside Rust itself. Even if in the first
|
||||
steps, it looks promising and the idea of a low-level language, without GC, with
|
||||
automatic memory management and so much safety having asynchronous support is
|
||||
exciting to me! 🥳 So the only option left, at this point, was to start messing
|
||||
around with it. I started by picking up one of the many frameworks that provides
|
||||
a layer for the Telegram APIs, [Teloxide](https://github.com/teloxide/teloxide).
|
||||
In particular, as you can see from its _README_, one of the examples starts by
|
||||
using `#[tokio:main]` macro:
|
||||
|
||||
```rust
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
teloxide::enable_logging!();
|
||||
log::info!("Starting dices_bot...");
|
||||
|
||||
let bot = Bot::from_env().auto_send();
|
||||
|
||||
teloxide::repl(bot, |message| async move {
|
||||
message.answer_dice().await?;
|
||||
respond(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
```
|
||||
|
||||
This was the reason I picked it up, given that it looked the most promising by
|
||||
the time I started the project.
|
||||
|
||||
### Building _Songlify_
|
||||
|
||||
So, after choosing what was going to use to build the bot, I needed a _reason_
|
||||
to build it.
|
||||
|
||||
With my friends we usually share a lot of songs (via Spotify links), so I
|
||||
thought it was a good idea to build a bot around it. I integrated a Spotify API
|
||||
library in it and started hacking up a bot.
|
||||
|
||||
> ⚠ Note that at the time of writing I have just noticed that the library I use
|
||||
> for speaking with Spotify, [aspotify](https://crates.io/crates/aspotify) has
|
||||
> been deprecated in favour of [rspotify](https://crates.io/crates/rspotify)
|
||||
|
||||
The first bot version was something very simple, and it was a single-file
|
||||
program with nothing very fancy (I have written it in a night):
|
||||
|
||||
```rust
|
||||
use crate::SpotifyURL::Track;
|
||||
use aspotify::{Client, ClientCredentials};
|
||||
use teloxide::prelude::*;
|
||||
|
||||
enum SpotifyURL {
|
||||
Track(String),
|
||||
}
|
||||
|
||||
fn get_spotify_entry(url: &str) -> Option<SpotifyURL> {
|
||||
if url.contains("https://open.spotify.com/track/") {
|
||||
let track_id = url.rsplit('/').next().and_then(|x| x.split('?').next());
|
||||
return match track_id {
|
||||
Some(id) => Some(SpotifyURL::Track(id.to_string())),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
struct TrackInfo {
|
||||
name: String,
|
||||
artist: Vec<String>,
|
||||
}
|
||||
|
||||
async fn get_spotify_track(spotify: Box<Client>, id: &String) -> Option<TrackInfo> {
|
||||
match spotify.tracks().get_track(id.as_str(), None).await {
|
||||
Ok(track) => Some(TrackInfo {
|
||||
name: track.data.name,
|
||||
artist: track.data.artists.iter().map(|x| x.name.clone()).collect(),
|
||||
}),
|
||||
Err(_e) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
teloxide::enable_logging!();
|
||||
log::info!("Starting Songlify...");
|
||||
|
||||
let bot = Bot::from_env().auto_send();
|
||||
teloxide::repl(bot, |message| async move {
|
||||
let spotify_creds =
|
||||
ClientCredentials::from_env().expect("CLIENT_ID and CLIENT_SECRET not found.");
|
||||
let spotify_client = Box::new(Client::new(spotify_creds));
|
||||
|
||||
log::info!("Connected to Spotify");
|
||||
let text = message.update.text().and_then(get_spotify_entry);
|
||||
match text {
|
||||
Some(spotify) => match spotify {
|
||||
Track(id) => {
|
||||
let track_info = get_spotify_track(spotify_client, &id).await;
|
||||
match track_info {
|
||||
Some(info) => {
|
||||
let reply = format!(
|
||||
"Track information:\n\
|
||||
Track name: {}\n\
|
||||
Artists: {}",
|
||||
info.name,
|
||||
info.artist.join(", ")
|
||||
);
|
||||
Some(message.reply_to(reply).await?)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
respond(())
|
||||
})
|
||||
.await;
|
||||
|
||||
log::info!("Exiting...");
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, basically every time a request arrived to the bot, login to
|
||||
Spotify was performed and track information and name retrieved from there. Of
|
||||
course this was only the beginning (also you can "appreciate" the number of
|
||||
nested blocks there...). Now the bot supports albums and playlists too, with the
|
||||
possibility to go through each song in the playlist and collect general
|
||||
information such as how many artists are in that playlist, how many songs and
|
||||
other little information like that. If you see the [bot
|
||||
repository](https://git.poldebra.me/polpetta/Songlify) you can see now that
|
||||
Spotify functions live in a separate module.
|
||||
|
||||
#### Packaging and distribution
|
||||
|
||||
The obvious choice for a software like that was to incorporate it into a OCI
|
||||
image. I wrote a very simple Dockerfile that, once the program was built, took
|
||||
the artifact and using the [multi-stage Docker build
|
||||
functionality](https://docs.docker.com/develop/develop-images/multistage-build/)
|
||||
and put it into a separate container, in order to avoid having build
|
||||
dependencies inside the final image. I used the images distributed by the
|
||||
[Distroless project](gcr.io/distroless/) (you can find the source on their
|
||||
[Github repository](https://github.com/GoogleContainerTools/distroless)) in
|
||||
order to obtain the smallest possible image. The final result?
|
||||
|
||||
```txt
|
||||
λ ~/Desktop/git/songlify/ docker images
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
test/test latest 8ac7a7018719 5 seconds ago 34MB
|
||||
<none> <none> 4bc7fb0699e0 12 seconds ago 1.53GB
|
||||
rust 1.56.1-slim-bullseye d3e070c5ffa7 6 weeks ago 667MB
|
||||
gcr.io/distroless/base latest-amd64 24787c1cd2e4 52 years ago 20.2MB
|
||||
```
|
||||
|
||||
A part from the 52 years old image pulled from `gcr`, you can see that
|
||||
`test/test` (actually Songlify) is only of **34MB**. Not much if you consider
|
||||
that inside that image there are shared dynamic libraries to make the executable
|
||||
able to run, which by default weights 20MB. A plus of these images is that they
|
||||
do not run as root user and they do not have any shell of bash integrated,
|
||||
making a possible surface attack smaller (not that Docker is secure anyway...).
|
||||
I upload the images on Docker Hub, where you can find them here
|
||||
<https://hub.docker.com/r/polpetta/songlify>
|
||||
|
||||
Finally, to run the bot I use a very simple docker-compose definition, that can
|
||||
be found in my [server-dotfiles
|
||||
repository](https://git.poldebra.me/polpetta/server-dotfiles/src/commit/7e7e1780b2db45f475510c49bf1a2f9e76c4c166/songlify/docker-compose.yml).
|
||||
This allows me to easily upgrade the bot by just changing the version and
|
||||
running `docker-compose up -d`.
|
||||
|
||||
#### Plans for the future
|
||||
|
||||
Currently I work on the bot only when I feel like I wanna add something. An
|
||||
interesting feature to add could be to insert a persistence layer (using a
|
||||
database for instance) and add various stats (which is the most shared song? Who
|
||||
_is that guy_ that shares the most songs in a group?). Persistence can be
|
||||
achieved quite easily by using [Diesel](https://diesel.rs/), an ORM compatible
|
||||
with various databases.
|
||||
|
||||
Another cool feature could be to add the _inline bot_ functionality, where you
|
||||
can search for songs directly in Spotify. Since I currently have a domain
|
||||
available for that I could set it up for receive web-hook notifications, instead
|
||||
of performing polling like the bot is currently doing (one requisite for inline
|
||||
bots is indeed to receive web-hooks). There are platforms like Heroku where you
|
||||
could make the bot run, but currently I prefer to use my box since it gives me
|
||||
more flexibility. Experimenting with Heroku could lead to cool results though
|
||||
😏.
|
||||
|
||||
Finally, link translation could be something very useful. I have a friend that
|
||||
does not use Spotify but prefers to listen to music via YouTube. So an
|
||||
interesting feature would be, given a Spotify link, to "convert" it into a
|
||||
YouTube link and, of course, _vice-versa_. This could lead to translate
|
||||
playlists and albums too into YouTube-based playlists, which of course could be
|
||||
very useful if you are trying to avoid the infamous _vendor lock-in_, in this
|
||||
case being stuck with Spotify because all you music collection, saved songs, etc
|
||||
is there.
|
Binary file not shown.
|
@ -36,3 +36,45 @@ dojo
|
|||
Gitea
|
||||
orchestrators
|
||||
md
|
||||
Spotify
|
||||
spotify
|
||||
ORM
|
||||
Songlify
|
||||
dotfiles
|
||||
txt
|
||||
amd
|
||||
cd
|
||||
gcr
|
||||
OCI
|
||||
songlify
|
||||
TorreArchimedeBot
|
||||
Subreddit
|
||||
JetBrains
|
||||
IntelliJ
|
||||
UI
|
||||
async
|
||||
GC
|
||||
README
|
||||
tokio
|
||||
teloxide
|
||||
fn
|
||||
env
|
||||
repl
|
||||
telegramscreen
|
||||
SpotifyURL
|
||||
aspotify
|
||||
ClientCredentials
|
||||
enum
|
||||
url
|
||||
str
|
||||
TrackInfo
|
||||
Ok
|
||||
TrackInfo
|
||||
Dockerfile
|
||||
Distroless
|
||||
bc
|
||||
fb
|
||||
ffa
|
||||
struct
|
||||
Vec
|
||||
creds
|
Loading…
Reference in New Issue