diff --git a/Cargo.lock b/Cargo.lock index fa12e74..762d13f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -832,6 +832,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "ipnet" version = "2.8.0" @@ -2302,6 +2308,7 @@ dependencies = [ "dashmap", "dotenvy", "humantime-serde", + "indoc", "log", "pretty_env_logger", "rand", diff --git a/Cargo.toml b/Cargo.toml index b75a6d3..f46f8e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] +license = "MIT" +edition = "2021" +version = "0.1.0" +readme = "README.md" name = "world-id-telegram" authors = ["Miguel Piedrafita "] -version = "0.1.0" -edition = "2021" repository = "https://github.com/worldcoin/world-id-telegram" -license = "MIT" description = "Add sybil-resistance to Telegram groups with World ID." keywords = [ "telegram", @@ -15,21 +16,21 @@ keywords = [ "world-id", "worldcoin", ] -readme = "README.md" [dependencies] log = "0.4" rand = "0.8" url = "2.4.1" +indoc = "2.0.4" dashmap = "5.5" +axum = "0.6.20" +dotenvy = "0.15.7" serde_with = "3.3" +reqwest = "0.11.22" +serde_json = "1.0.108" humantime-serde = "1.1" pretty_env_logger = "0.5" tokio = { version = "1.32", features = ["full"] } config = { version = "0.13", features = ["toml"] } serde = { version = "1.0", features = ["derive"] } teloxide = { version = "0.12", features = ["macros"] } -dotenvy = "0.15.7" -axum = "0.6.20" -reqwest = "0.11.22" -serde_json = "1.0.108" diff --git a/src/bot/commands.rs b/src/bot/commands.rs index 67a33ba..4160375 100644 --- a/src/bot/commands.rs +++ b/src/bot/commands.rs @@ -7,7 +7,10 @@ use teloxide::{ Bot, }; -use crate::{bot::HandlerResult, config::GroupsConfig}; +use crate::{ + bot::HandlerResult, + config::{AppConfig, GroupsConfig}, +}; #[derive(BotCommands)] #[command(rename_rule = "lowercase", description = "Available commands:")] @@ -22,7 +25,7 @@ pub enum Command { pub async fn command_handler( bot: Bot, - config: Arc, + config: Arc, msg: Message, me: Me, text: String, @@ -31,8 +34,8 @@ pub async fn command_handler( return Ok(()); } - if !config.is_group_allowed(msg.chat.id) { - return on_group_not_allowed(bot, config, msg).await; + if !config.groups_config.is_group_allowed(msg.chat.id) { + return on_group_not_allowed(bot, &config.groups_config, msg).await; } let Ok(command) = BotCommands::parse(text.as_str(), me.username()) else { @@ -77,11 +80,7 @@ pub async fn command_handler( Ok(()) } -pub async fn on_group_not_allowed( - bot: Bot, - config: Arc, - msg: Message, -) -> HandlerResult { +pub async fn on_group_not_allowed(bot: Bot, config: &GroupsConfig, msg: Message) -> HandlerResult { log::error!( "Unknown chat {} with id {}", msg.chat.title().unwrap_or_default(), diff --git a/src/bot/join_check/mod.rs b/src/bot/join_check/mod.rs index 151a703..409ecfb 100644 --- a/src/bot/join_check/mod.rs +++ b/src/bot/join_check/mod.rs @@ -1,28 +1,28 @@ use std::sync::Arc; + use teloxide::{ prelude::*, types::{ChatPermissions, InlineKeyboardButton, InlineKeyboardMarkup, MessageId, User}, utils::html::escape, }; -use url::Url; use crate::{ bot::{commands::on_group_not_allowed, HandlerResult, JoinRequest, JoinRequests}, - config::GroupsConfig, + config::AppConfig, }; pub async fn join_handler( bot: Bot, msg: Message, users: Vec, - config: Arc, + config: Arc, join_requests: JoinRequests, ) -> HandlerResult { - if !config.is_group_allowed(msg.chat.id) { - return on_group_not_allowed(bot, config, msg).await; + if !config.groups_config.is_group_allowed(msg.chat.id) { + return on_group_not_allowed(bot, &config.groups_config, msg).await; } - let chat_cfg = config.get(msg.chat.id); + let chat_cfg = config.groups_config.get(msg.chat.id); for user in users { let join_requests = join_requests.clone(); @@ -44,7 +44,9 @@ pub async fn join_handler( let verify_button = InlineKeyboardButton::url( "Verify with World ID", - Url::parse("https://example.com").unwrap(), + config + .app_url + .join(&format!("verify/{}/{}", msg.chat.id, msg.id))?, ); let msg_id = bot @@ -55,7 +57,7 @@ pub async fn join_handler( .await? .id; - join_requests.insert(msg_id, JoinRequest::new(user.id, msg.chat.id)); + join_requests.insert((msg.chat.id, msg_id), JoinRequest::new(user.id)); tokio::spawn({ let bot = bot.clone(); @@ -63,7 +65,7 @@ pub async fn join_handler( async move { tokio::time::sleep(ban_after).await; - if let Some((_, data)) = join_requests.remove(&msg_id) { + if let Some((_, data)) = join_requests.remove(&(msg.chat.id, msg_id)) { if !data.is_verified { bot.ban_chat_member(msg.chat.id, data.user_id) .await @@ -83,23 +85,24 @@ pub async fn join_handler( pub async fn on_verified( bot: Bot, + chat_id: ChatId, msg_id: MessageId, join_requests: JoinRequests, ) -> HandlerResult { let mut join_req = join_requests - .get_mut(&msg_id) + .get_mut(&(chat_id, msg_id)) .ok_or("Can't find the message id in group dialogue")?; - let Some(permissions) = bot.get_chat(join_req.chat_id).await?.permissions() else { + let Some(permissions) = bot.get_chat(chat_id).await?.permissions() else { return Err("Can't get the group permissions".into()); }; join_req.is_verified = true; - bot.restrict_chat_member(join_req.chat_id, join_req.user_id, permissions) + bot.restrict_chat_member(chat_id, join_req.user_id, permissions) .await?; - bot.delete_message(join_req.chat_id, msg_id).await?; + bot.delete_message(chat_id, msg_id).await?; Ok(()) } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index aa0e9dd..1fbd5ae 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -16,21 +16,19 @@ mod commands; mod join_check; type HandlerResult = Result<(), HandlerError>; -pub type JoinRequests = Arc>; +pub type JoinRequests = Arc>; type HandlerError = Box; #[derive(Clone)] pub struct JoinRequest { pub user_id: UserId, - pub chat_id: ChatId, pub is_verified: bool, } impl JoinRequest { - fn new(user_id: UserId, chat_id: ChatId) -> Self { + fn new(user_id: UserId) -> Self { Self { user_id, - chat_id, is_verified: false, } } @@ -51,7 +49,7 @@ pub async fn start(bot: Bot, config: AppConfig, join_requests: JoinRequests) { Dispatcher::builder(bot, handler) .default_handler(|_| async {}) - .dependencies(dptree::deps![Arc::new(config.groups_config), join_requests]) + .dependencies(dptree::deps![Arc::new(config), join_requests]) .enable_ctrlc_handler() .build() .dispatch() diff --git a/src/config.rs b/src/config.rs index 4af0d39..152db75 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,9 +6,11 @@ use teloxide::{ types::{ChatId, User, UserId}, utils::html::escape, }; +use url::Url; -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct AppConfig { + pub app_url: Url, pub app_id: String, pub bot_token: String, #[serde(flatten, default)] @@ -85,7 +87,12 @@ pub struct MessagesText { impl MessagesText { pub fn create_welcome_msg(&self, user: &User, chat_name: &str) -> String { self.new_user_template - .replace("{TAGUSER}", &user.mention().unwrap()) + .replace( + "{TAGUSER}", + &user.mention().unwrap_or_else(|| { + format!("[{}](tg://user?id={})", user.full_name(), user.id,) + }), + ) .replace("{CHATNAME}", &escape(chat_name)) } } diff --git a/src/main.rs b/src/main.rs index 6c8ebdb..be91ffd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use dashmap::DashMap; use dotenvy::dotenv; use std::sync::Arc; -use teloxide::{requests::Requester, types::MessageId, Bot}; +use teloxide::{ + requests::Requester, + types::{ChatId, MessageId}, + Bot, +}; use crate::{bot::JoinRequest, config::AppConfig}; @@ -15,7 +19,7 @@ async fn main() { pretty_env_logger::init(); let config = AppConfig::try_read().expect("Failed to read config"); - let join_requests = Arc::new(DashMap::::new()); + let join_requests = Arc::new(DashMap::<(ChatId, MessageId), JoinRequest>::new()); let bot = Bot::new(&config.bot_token); let bot_data = bot.get_me().await.expect("Failed to get bot account"); diff --git a/src/server/mod.rs b/src/server/mod.rs index d7a1967..c17f98e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,10 +1,15 @@ use axum::{ - extract::Path, http::StatusCode, response::Redirect, routing::get, Extension, Json, Router, + extract::Path, + http::StatusCode, + response::{Html, Redirect}, + routing::get, + Extension, Json, Router, }; +use indoc::formatdoc; use serde_json::json; use std::net::SocketAddr; use teloxide::{ - types::{MessageId, User}, + types::{ChatId, MessageId, User}, Bot, }; use tokio::signal; @@ -22,7 +27,10 @@ pub async fn start(bot: Bot, config: AppConfig, bot_data: User, join_requests: J Redirect::permanent(&format!("https://t.me/{}", bot_data.username.unwrap())) }), ) - .route("/verify/:msg_id", get(verify_page).post(verify_api)) + .route( + "/verify/:chat_id/:msg_id", + get(verify_page).post(verify_api), + ) .layer(Extension(bot)) .layer(Extension(config)) .layer(Extension(join_requests)); @@ -38,14 +46,50 @@ pub async fn start(bot: Bot, config: AppConfig, bot_data: User, join_requests: J } async fn verify_page( + Path(chat_id): Path, Path(msg_id): Path, + Extension(config): Extension, Extension(join_reqs): Extension, -) -> Result<&'static str, StatusCode> { - let _join_req = join_reqs.get(&msg_id).ok_or(StatusCode::NOT_FOUND)?; +) -> Result, StatusCode> { + if !join_reqs.contains_key(&(chat_id, msg_id)) { + return Err(StatusCode::NOT_FOUND); + } + + let page = formatdoc! {" + + + + + + Verify with World ID + + + + + + + ", app_id = config.app_id + }; - Ok("Hello, World!") + Ok(Html(page)) } #[derive(Debug, serde::Deserialize)] @@ -58,12 +102,15 @@ struct VerifyRequest { async fn verify_api( Extension(bot): Extension, + Path(chat_id): Path, Path(msg_id): Path, Extension(config): Extension, Extension(join_reqs): Extension, Json(req): Json, ) -> Result<&'static str, StatusCode> { - let join_req = join_reqs.get(&msg_id).ok_or(StatusCode::NOT_FOUND)?; + let join_req = join_reqs + .get(&(chat_id, msg_id)) + .ok_or(StatusCode::NOT_FOUND)?; reqwest::Client::new() .post(format!( @@ -72,8 +119,8 @@ async fn verify_api( )) .json(&json!({ "signal": msg_id, + "action": chat_id, "proof": req.proof, - "action": join_req.chat_id, "merkle_root": req.merkle_root, "nullifier_hash": req.nullifier_hash, "credential_type": req.credential_type, @@ -86,7 +133,7 @@ async fn verify_api( drop(join_req); - on_verified(bot, msg_id, join_reqs) + on_verified(bot, chat_id, msg_id, join_reqs) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;