Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telegram bot #19

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,859 changes: 1,859 additions & 0 deletions examples/bots/telegram-bot/Cargo.lock

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions examples/bots/telegram-bot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "telegram-bot"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
teloxide = { version = "0.12", features = ["macros"] }
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.29", features = ["rt-multi-thread", "macros"] }
clap = { version = "4", features = ["derive", "env"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
some-verifier-lib = { version = "0.1", path = "../../some-verifier-lib" }
36 changes: 36 additions & 0 deletions examples/bots/telegram-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Telegram Bot

Telegram bot that can redirect users to the verification dApp and check the verification status.

The bot repsonds to the following commands:

```
/start - starts a new chat.
/help - displays the list of commands.
/verify - sends a link to the SoMe verifier dApp.
/check - used in reply to another message. Replies with the verification status of the sender.
```

## Usage

Arguments:

```
--token <BOT_TOKEN>
Telegram bot API token. [env: TELEGRAM_BOT_TOKEN=]
--log-level <LOG_LEVEL>
Maximum log level. [env: TELEGRAM_BOT_LOG_LEVEL=] [default: info]
--request-timeout <REQUEST_TIMEOUT>
Request timeout in milliseconds. [env: TELEGRAM_BOT_REQUEST_TIMEOUT=] [default: 5000]
--verifier-url <VERIFIER_URL>
URL of the SoMe verifier. [env: TELEGRAM_BOT_VERIFIER_URL=] [default: http://127.0.0.1/]
```

In order for the "Login with Telegram" feature on the dApp to work, the bot needs to have its "domain" set (to match `--verifier-url`).
This can be configured by messaging @BotFather on Telegram, see [https://core.telegram.org/widgets/login](https://core.telegram.org/widgets/login).

The bot relies on a verification check service, given by `--verifier-url`.
This is also the URL that the bot will link to.

Note: When running locally `--verifier-url` must be set to `http://127.0.0.1/`,
if `http://localhost/` is used, the bot refuses to send a link.
213 changes: 213 additions & 0 deletions examples/bots/telegram-bot/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::sync::Arc;
use std::time::Duration;

use anyhow::Context;
use clap::Parser;
use reqwest::Url;
use some_verifier_lib::{Platform, Verification};
use teloxide::dispatching::UpdateHandler;
use teloxide::types::{InlineKeyboardButton, MessageKind, ReplyMarkup, User};
use teloxide::RequestError;
use teloxide::{prelude::*, utils::command::BotCommands};

#[derive(clap::Parser, Debug)]
#[clap(arg_required_else_help(true))]
#[clap(version, author)]
struct App {
#[clap(
long = "token",
help = "Telegram bot API token.",
env = "TELEGRAM_BOT_TOKEN"
)]
bot_token: String,
#[clap(
long = "log-level",
default_value = "info",
help = "Maximum log level.",
env = "TELEGRAM_BOT_LOG_LEVEL"
)]
log_level: tracing_subscriber::filter::LevelFilter,
#[clap(
long = "request-timeout",
help = "Request timeout in milliseconds.",
default_value = "5000",
env = "TELEGRAM_BOT_REQUEST_TIMEOUT"
)]
request_timeout: u64,
#[clap(
long = "verifier-url",
default_value = "http://127.0.0.1/",
help = "URL of the SoMe verifier.",
env = "TELEGRAM_BOT_VERIFIER_URL"
)]
verifier_url: Url,
}

#[derive(BotCommands, Clone)]
#[command(
rename_rule = "lowercase",
description = "The following commands are supported:"
)]
enum Command {
#[command(description = "start a new chat.")]
Start,
#[command(description = "show available commands.")]
Help,
#[command(description = "verify your Telegram account.")]
Verify,
#[command(description = "use in reply to a message, checks if account is verified.")]
Check,
}

#[derive(Clone)]
struct BotConfig {
verifier_url: Arc<Url>,
client: reqwest::Client,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let app = App::parse();

{
use tracing_subscriber::prelude::*;
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(app.log_level)
.init();
}

tracing::info!("Starting Telegram bot...");

let client = reqwest::Client::builder()
.timeout(Duration::from_millis(app.request_timeout))
.build()
.context("Failed to start HTTP server.")?;

let bot = Bot::new(app.bot_token);
let cfg = BotConfig {
verifier_url: Arc::new(app.verifier_url),
client,
};
bot.set_my_commands(Command::bot_commands()).await?;

Dispatcher::builder(bot, schema())
.dependencies(dptree::deps![cfg])
.error_handler(LoggingErrorHandler::with_custom_text(
"An error has occurred in the dispatcher",
))
.enable_ctrlc_handler()
.build()
.dispatch()
.await;

Ok(())
}

fn schema() -> UpdateHandler<RequestError> {
use dptree::case;
let command_handler = teloxide::filter_command::<Command, _>()
.branch(case![Command::Start].endpoint(help))
.branch(case![Command::Help].endpoint(help))
.branch(case![Command::Verify].endpoint(verify))
.branch(case![Command::Check].endpoint(check));

Update::filter_message()
.branch(command_handler)
.endpoint(other)
}

/// Handlers for the `/help` and `/start` commands.
async fn help(bot: Bot, msg: Message) -> ResponseResult<()> {
bot.send_message(msg.chat.id, Command::descriptions().to_string())
.await?;
Ok(())
}

/// Handler for the `/verify` command.
async fn verify(cfg: BotConfig, bot: Bot, msg: Message) -> ResponseResult<()> {
let dapp_url = cfg.verifier_url.as_ref().clone();
let verify_button = ReplyMarkup::inline_kb([[InlineKeyboardButton::url("Verify", dapp_url)]]);
bot.send_message(msg.chat.id, "Please verify with your wallet.")
.reply_markup(verify_button)
.await?;

Ok(())
}

/// Handler for the `/check` command. This must be used in reply to another message.
async fn check(cfg: BotConfig, bot: Bot, msg: Message) -> ResponseResult<()> {
if let Some(target_msg) = msg.reply_to_message() {
if let Some(target_user) = target_msg.from() {
check_user(cfg, bot, &msg, target_user).await?;
} else {
bot.send_message(msg.chat.id, "/check can not be used in channels.")
.await?;
}
} else {
bot.send_message(msg.chat.id, "Usage: reply /check to a message.")
.await?;
}

Ok(())
}

/// Checks the verification status of a given user and sends a message with the result.
async fn check_user(
cfg: BotConfig,
bot: Bot,
msg: &Message,
target_user: &User,
) -> ResponseResult<()> {
match get_verifications(cfg, target_user.id).await {
Ok(verifications) => {
let name = target_user.mention().unwrap_or(target_user.full_name());
let reply = if verifications.is_empty() {
format!("{name} is not verified with Concordium.")
} else {
let mut reply = format!("{name} is verified with Concordium.");
for verification in verifications
.into_iter()
.filter(|v| v.platform != Platform::Telegram && !v.revoked)
{
reply.push_str(&format!(
"\n- {}: {}",
verification.platform, verification.username
));
}
reply
};
bot.send_message(msg.chat.id, reply)
.reply_to_message_id(msg.id)
.await?;
}
Err(err) => tracing::error!("{err}"),
}

Ok(())
}

/// Fallback handler.
async fn other(bot: Bot, msg: Message) -> ResponseResult<()> {
// If not direct message to bot
if !matches!(msg.kind, MessageKind::Common(_)) || !msg.chat.is_private() {
return Ok(());
}

bot.send_message(
msg.chat.id,
"Unrecognized command, type /help to see available commands.",
)
.await?;
Ok(())
}

async fn get_verifications(cfg: BotConfig, id: UserId) -> anyhow::Result<Vec<Verification>> {
let url = cfg
.verifier_url
.join("verifications/telegram/")
.expect("URLs can be joined with a string path")
.join(&id.to_string())
.expect("URLs can be joined with a UserId");
Ok(cfg.client.get(url).send().await?.json().await?)
}
65 changes: 65 additions & 0 deletions examples/some-verifier-lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions examples/some-verifier-lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "some-verifier-lib"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0.173", features = ["derive"] }
25 changes: 25 additions & 0 deletions examples/some-verifier-lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::fmt::{self, Display, Formatter};

lassemoldrup marked this conversation as resolved.
Show resolved Hide resolved
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
Telegram,
Discord,
}

impl Display for Platform {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Telegram => write!(f, "Telegram"),
Self::Discord => write!(f, "Discord"),
}
}
}

#[derive(Serialize, Deserialize)]
pub struct Verification {
pub platform: Platform,
pub username: String,
pub revoked: bool,
}
lassemoldrup marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions examples/some-verifier/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tddb
lassemoldrup marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading