├── .gitignore ├── misc └── systemd │ └── raf@.service ├── src ├── lib.rs ├── persistence │ ├── mod.rs │ ├── db.rs │ └── types.rs ├── telegram │ ├── mod.rs │ ├── users.rs │ ├── messages.rs │ ├── channels.rs │ ├── contests.rs │ ├── commands.rs │ └── handlers.rs └── bin │ └── raf.rs ├── Cargo.toml ├── README.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | raf.db 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /misc/systemd/raf@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=RaF - Refer A Friend contest creator for Telegram Channels 3 | Documentation=https://github.com/galeone/raf 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Restart=on-failure 9 | User=%I 10 | Type=simple 11 | EnvironmentFile=/home/%I/.raf/raf.env 12 | WorkingDirectory=/home/%I/.raf/ 13 | ExecStart=/home/%I/.cargo/bin/raf 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![warn(clippy::pedantic)] 16 | 17 | pub mod persistence; 18 | pub mod telegram; 19 | -------------------------------------------------------------------------------- /src/persistence/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Persistence crate. It contains the schema definition and creation, together 16 | //! with the utility function for creating the connection pool and the 17 | //! struct <-> tables mapping (in the `types` module). 18 | pub mod db; 19 | pub mod types; 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "telegram-raf" 3 | version = "0.1.3" 4 | description = "RaF (Refer a Friend): bot for creating referral-based contests for your Telegram channels, groups and supergroups" 5 | readme = "README.md" 6 | authors = ["Paolo Galeone "] 7 | edition = "2018" 8 | default-run = "raf" 9 | repository = "https://github.com/galeone/raf" 10 | license = "Apache-2.0" 11 | keywords = ["telegram", "bot", "referral"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | chrono = "0.4.42" 16 | data-encoding = "2.9.0" 17 | default = "0.1.2" 18 | hyper = "1.8.1" 19 | hyper-tls = "0.6.0" 20 | log = "0.4.28" 21 | r2d2 = "0.8.10" 22 | r2d2_sqlite = "0.31.0" 23 | regex = "1.12.2" 24 | simple_logger = "5.1.0" 25 | tabular = "0.2.0" 26 | typemap = "0.3.3" 27 | url = "2.5.7" 28 | telexide-fork = "0.2.5" 29 | 30 | [dependencies.rusqlite] 31 | features = ["chrono"] 32 | version = "0.37.0" 33 | 34 | [dependencies.tokio] 35 | features = ["full"] 36 | version = "1.48.0" 37 | -------------------------------------------------------------------------------- /src/telegram/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! `RaF` telegram communication. 16 | //! 17 | //! This create is based upon a [fork (galeone/telexide) ](https://github.com/galeone/telexide/) of [telexide](https://github.com/CalliEve/telexide). 18 | //! The fork made it possible to use the library, since it was not maintained anymore and it was 19 | //! using a very old version of tokio. 20 | //! 21 | //! It uses the `raf::persistence` crate too, since every action executed remotely can have some 22 | //! local effect on the `RaF` storage. 23 | //! 24 | //! # What's inside this crate? 25 | //! 26 | //! - `channels`: functions for working with channels, like registering the channels to `RaF` or 27 | //! getting the channels info. Despite the name, also groups and supergroups are supported, even 28 | //! though they are always considered channels. Under the hood, there's almost zero differences 29 | //! from the `RaF` goal. 30 | //! - `commands`: the commands available to the `RaF` users, like `/start`, `/rank`, `/contest`. See 31 | //! `/help` for the complete list of commands. 32 | //! - `contests`: function for creating and updating the contests. The complete contest workflow is 33 | //! not here, but in the `handlers` crate - because of how Telegram (and Telexide) works. 34 | //! - `handlers`: the handlers for callback events (buttons, user interactions) and user messages. 35 | //! - `messages`: functions for managing the text messages, like sending the `RaF` menu, working with 36 | //! markdown, ... 37 | //! - `users`: functions for getting a specific users or all the users that are channel owners. 38 | 39 | pub mod channels; 40 | pub mod commands; 41 | pub mod contests; 42 | pub mod handlers; 43 | pub mod messages; 44 | pub mod users; 45 | -------------------------------------------------------------------------------- /src/telegram/users.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use rusqlite::params; 16 | use telexide_fork::prelude::*; 17 | 18 | use crate::persistence::types::{DBKey, User}; 19 | 20 | /// Returns the `User` with the specified `id`, if any. 21 | /// 22 | /// # Arguments 23 | /// * `ctx` - Telexide context 24 | /// * `id` - The user ud 25 | /// 26 | /// # Panics 27 | /// Panics if the connection to the db fails. 28 | #[must_use] 29 | pub fn get(ctx: &Context, id: i64) -> Option { 30 | let guard = ctx.data.read(); 31 | let map = guard.get::().expect("db"); 32 | let conn = map.get().unwrap(); 33 | let mut stmt = conn 34 | .prepare("SELECT first_name, last_name, username FROM users WHERE id = ?") 35 | .unwrap(); 36 | let mut iter = stmt 37 | .query_map(params![id], |row| { 38 | Ok(User { 39 | id, 40 | first_name: row.get(0)?, 41 | last_name: row.get(1)?, 42 | username: row.get(2)?, 43 | }) 44 | }) 45 | .unwrap(); 46 | if let Some(user) = iter.next() { 47 | return match user { 48 | Ok(user) => Some(user), 49 | Err(_) => None, 50 | }; 51 | } 52 | None 53 | } 54 | 55 | /// Returns the complete list of owners. Owners are the users who registered a channel/group. 56 | /// 57 | /// # Arguments 58 | /// * `ctx` - Telexide context 59 | /// 60 | /// # Panics 61 | /// Panics if the connection to the db fails. 62 | #[must_use] 63 | pub fn owners(ctx: &Context) -> Vec { 64 | let guard = ctx.data.read(); 65 | let map = guard.get::().expect("db"); 66 | let conn = map.get().unwrap(); 67 | let mut stmt = conn 68 | .prepare( 69 | "SELECT DISTINCT users.id, users.first_name, users.last_name, users.username \ 70 | FROM users INNER JOIN channels ON users.id = channels.registered_by \ 71 | ORDER BY users.id", 72 | ) 73 | .unwrap(); 74 | 75 | let users = stmt 76 | .query_map(params![], |row| { 77 | Ok(User { 78 | id: row.get(0)?, 79 | first_name: row.get(1)?, 80 | last_name: row.get(2)?, 81 | username: row.get(3)?, 82 | }) 83 | }) 84 | .unwrap() 85 | .map(Result::unwrap) 86 | .collect(); 87 | users 88 | } 89 | -------------------------------------------------------------------------------- /src/bin/raf.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::env; 16 | use telexide_fork::{api::types::*, prelude::*}; 17 | 18 | use log::{error, LevelFilter}; 19 | use simple_logger::SimpleLogger; 20 | 21 | use tokio::time::{sleep, Duration}; 22 | 23 | use telegram_raf::persistence::db::connection; 24 | use telegram_raf::persistence::types::*; 25 | 26 | use telegram_raf::telegram::commands::*; 27 | use telegram_raf::telegram::handlers; 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | SimpleLogger::new() 32 | .with_level(LevelFilter::Info) 33 | .init() 34 | .unwrap(); 35 | 36 | let pool = connection(); 37 | let token = env::var("TOKEN").expect("Provide the token via TOKEN env var"); 38 | let bot_name = env::var("BOT_NAME").expect("Provide the bot name via BOT_NAME env var"); 39 | 40 | // Check for the --broadcast flag 41 | let mut broadcast = false; 42 | let args: Vec = env::args().collect(); 43 | if args.len() > 1 && args[1] == "--broadcast" { 44 | broadcast = true; 45 | } 46 | 47 | let mut binding = ClientBuilder::new(); 48 | let mut client_builder = binding.set_token(&token); 49 | 50 | if broadcast { 51 | client_builder = client_builder.set_framework(create_framework!(&bot_name, broadcast)) 52 | } else { 53 | client_builder = client_builder 54 | .set_framework(create_framework!( 55 | &bot_name, help, start, register, contest, list, rank 56 | )) 57 | .set_allowed_updates(vec![UpdateType::CallbackQuery, UpdateType::Message]) 58 | .add_handler_func(handlers::message) 59 | .add_handler_func(handlers::callback); 60 | } 61 | 62 | let client = client_builder.build(); 63 | 64 | { 65 | let mut data = client.data.write(); 66 | data.insert::(pool); 67 | data.insert::(bot_name); 68 | } 69 | 70 | if broadcast { 71 | let ret = client.start().await; 72 | match ret { 73 | Err(err) => { 74 | error!("ApiResponse {}\nWaiting a minute and retrying...", err); 75 | sleep(Duration::from_secs(60)).await; 76 | } 77 | Ok(()) => { 78 | error!("Exiting from main loop without an error, but this should never happen!"); 79 | } 80 | } 81 | } else { 82 | loop { 83 | let ret = client.start().await; 84 | match ret { 85 | Err(err) => { 86 | error!("ApiResponse {}\nWaiting a minute and retrying...", err); 87 | sleep(Duration::from_secs(60)).await; 88 | } 89 | Ok(()) => { 90 | error!( 91 | "Exiting from main loop without an error, but this should never happen!" 92 | ); 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram RaF \[Refer a Friend\]([@RefafBot](https://t.me/RefafBot)) 2 | 3 | RaF is a bot for creating referral-based contests for your Telegram channels, groups and supergroups. 4 | 5 | Create contests, let your users share their link to your channel/group, increase your audience, and give prizes to the winners! 6 | 7 | --- 8 | 9 | ## Introduction 10 | 11 | The software is written in [rust](https://github.com/rust-lang/rust). Raf depends on [a fork of telexide](https://github.com/galeone/telexide), a rust library for making telegram bots. The fork makes the original library work and solves some issues. 12 | 13 | The storage used is SQLite: RaF creates a `raf.db` file in its run path where it saves all the relationships between: 14 | 15 | - Who owns the channels 16 | - The contests created 17 | - The invitations each participant generated 18 | - The users who joined the channel through an invitation 19 | 20 | ## Setup 21 | 22 | 1. Install RaF 23 | 24 | For the development version: 25 | 26 | ```bash 27 | cargo install --path . 28 | ``` 29 | 30 | 31 | 32 | For the production version: 33 | 34 | ```bash 35 | cargo install telegram-raf 36 | ``` 37 | 38 | 2. Create the run path and the environment file 39 | 40 | ```bash 41 | mkdir $HOME/.raf 42 | 43 | echo 'BOT_NAME=""' > $HOME/.raf/raf.env 44 | echo 'TOKEN=""' >> $HOME/.raf/raf.env 45 | ``` 46 | 47 | 3. Copy the systemd service file 48 | 49 | ```bash 50 | sudo cp misc/systemd/raf@.service /lib/systemd/system/ 51 | ``` 52 | 53 | 4. Start and enable the service 54 | 55 | ```bash 56 | sudo systemctl start raf@$USER.service 57 | sudo systemctl enable raf@$USER.service 58 | ``` 59 | 60 | The `raf.db` (to backup or inspect) is in `$HOME/.raf/`. 61 | 62 | ### Broadcast Feature 63 | 64 | The bot supports a broadcast feature that allows the bot owner to send messages to all users and channels. To use this feature: 65 | 66 | 1. Create a `broadcast.md` file in the bot's run directory (`$HOME/.raf/`) with the message you want to broadcast. The message supports Markdown formatting. 67 | 68 | 2. If the bot is currently running, stop it. It requires a separate instance. Now start the bot with the `--broadcast` flag: 69 | ```bash 70 | raf --broadcast 71 | ``` 72 | 73 | 3. Once the bot is running, use the `/broadcast` command to send the message from `broadcast.md` to all users and channels. 74 | 4. You can restart the bot to make it work as usual. 75 | 76 | The `broadcast.md` file should be formatted using Markdown V2 syntax, as the bot will send the message with `ParseMode::MarkdownV2`. 77 | 78 | ## Contributing 79 | 80 | Any feedback is welcome. Feel free to open issues and create pull requests! 81 | 82 | 83 | ## License 84 | 85 | ``` 86 | Copyright 2021 Paolo Galeone 87 | 88 | Licensed under the Apache License, Version 2.0 (the "License"); 89 | you may not use this file except in compliance with the License. 90 | You may obtain a copy of the License at 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | ``` 100 | -------------------------------------------------------------------------------- /src/persistence/db.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use r2d2_sqlite::SqliteConnectionManager; 16 | 17 | /// Database schema definition. Transaction executed every time a new connection 18 | /// pool is requested (usually, once at the application startup). 19 | /// 20 | /// `being_managed_channels`, as the name suggests, is the channel that the owner ( 21 | /// hence `channels.registered_by` == owner) is managing. 22 | /// 23 | /// NOTE: `being_contacted_users` and `being_managed_channels` are tables required because 24 | /// there are moments in the flow, where the user should send "complex" messages, but these 25 | /// "complex" messages are outside the FSM created by the `callback_handler` 26 | /// (FSM created naturally because all the callbacks invokes the same method). 27 | const SCHEMA: &str = "BEGIN; 28 | CREATE TABLE IF NOT EXISTS users ( 29 | id INTEGER PRIMARY KEY NOT NULL, 30 | first_name TEXT NOT NULL, 31 | last_name TEXT, 32 | username TEXT 33 | ); 34 | CREATE TABLE IF NOT EXISTS channels ( 35 | id INTEGER PRIMARY KEY NOT NULL, 36 | registered_by INTEGER NOT NULL, 37 | link TEXT NOT NULL, 38 | name TEXT NOT NULL, 39 | FOREIGN KEY(registered_by) REFERENCES users(id), 40 | UNIQUE(id, registered_by) 41 | ); 42 | CREATE TABLE IF NOT EXISTS invitations( 43 | id INTEGER PRIMARY KEY AUTOINCREMENT, 44 | date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 45 | source INTEGER NOT NULL, 46 | dest INTEGER NOT NULL, 47 | chan INTEGER NOT NULL, 48 | contest INTEGER NOT NULL, 49 | FOREIGN KEY(source) REFERENCES users(id), 50 | FOREIGN KEY(dest) REFERENCES users(id), 51 | FOREIGN KEY(chan) REFERENCES channels(id), 52 | FOREIGN KEY(contest) REFERENCES contests(id), 53 | CHECK (source <> dest), 54 | UNIQUE(source, dest, chan) 55 | ); 56 | CREATE TABLE IF NOT EXISTS contests( 57 | id INTEGER PRIMARY KEY AUTOINCREMENT, 58 | name TEXT NOT NULL, 59 | prize TEXT NOT NULL, 60 | end TIMESTAMP NOT NULL, 61 | chan INTEGER NOT NULL, 62 | started_at TIMESTAMP NULL, 63 | stopped BOOL NOT NULL DEFAULT FALSE, 64 | FOREIGN KEY(chan) REFERENCES channels(id), 65 | UNIQUE(name, chan) 66 | ); 67 | CREATE TABLE IF NOT EXISTS being_managed_channels( 68 | id INTEGER PRIMARY KEY AUTOINCREMENT, 69 | chan INTEGER NOT NULL, 70 | FOREIGN KEY(chan) REFERENCES channels(id) 71 | ); 72 | CREATE TABLE IF NOT EXISTS being_contacted_users( 73 | id INTEGER PRIMARY KEY AUTOINCREMENT, 74 | user INTEGER NOT NULL, 75 | owner INTEGER NOT NULL, 76 | contest INTEGER NOT NULL, 77 | contacted BOOL NOT NULL DEFAULT FALSE, 78 | FOREIGN KEY(user) REFERENCES users(id), 79 | FOREIGN KEY(owner) REFERENCES users(id) 80 | ); 81 | COMMIT;"; 82 | 83 | /// Creates a connection pool to the `SQLite` database, whose name is always 84 | /// "raf.db" and it's always in the current working directory of the application. 85 | /// 86 | /// Foreign keys are enabled in the `SQLite` instance. 87 | /// 88 | /// # Panics 89 | /// Panics if the connection with the db fails. 90 | #[must_use] 91 | pub fn connection() -> r2d2::Pool { 92 | let manager = SqliteConnectionManager::file("raf.db") 93 | .with_init(|c| c.execute_batch("PRAGMA foreign_keys=1;")); 94 | let pool = r2d2::Pool::builder().max_size(15).build(manager).unwrap(); 95 | { 96 | let conn = pool.get().unwrap(); 97 | conn.execute_batch(SCHEMA).unwrap(); 98 | } 99 | 100 | pool 101 | } 102 | -------------------------------------------------------------------------------- /src/persistence/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use chrono::DateTime; 16 | use chrono::Utc; 17 | use r2d2_sqlite::SqliteConnectionManager; 18 | use typemap::Key; 19 | 20 | /// A User is a human using the bot 21 | #[derive(Debug, Clone)] 22 | pub struct User { 23 | /// User unique ID, Telegram generated 24 | pub id: i64, 25 | /// User first name, mandatory from telegram API 26 | pub first_name: String, 27 | /// User last name, optional 28 | pub last_name: Option, 29 | /// User username, optional 30 | pub username: Option, 31 | } 32 | 33 | /// The Channel structure is used for identifying both 34 | /// channels and (super)groups, since they have the very same attributes. 35 | #[derive(Debug)] 36 | pub struct Channel { 37 | /// Channel unique ID, Telegram generated. 38 | pub id: i64, 39 | /// User unique ID, the user that registered the channel to the bot. 40 | /// Almost always the channel creator. 41 | pub registered_by: i64, 42 | /// The invitation link for the channel. Can be the user-decided one 43 | /// or the private-link Telegram generated. 44 | pub link: String, 45 | /// Channel name 46 | pub name: String, 47 | } 48 | 49 | /// A reference to the user that's currently managing a channel. 50 | #[derive(Debug)] 51 | pub struct BeingManagedChannel { 52 | /// User unique ID 53 | pub chan: i64, 54 | } 55 | 56 | /// An invitation sent from source, to dest, for the chan. 57 | #[derive(Debug)] 58 | pub struct Invite { 59 | /// Invitation unique ID, locally generated 60 | pub id: i64, 61 | /// Whenever the invitation has been created 62 | pub date: DateTime, 63 | /// The user who's inviting 64 | pub source: i64, 65 | /// The user who's being invited 66 | pub dest: i64, 67 | /// The channel dest user is being invited into 68 | pub chan: i64, 69 | } 70 | 71 | /// A referral based strategy contest 72 | #[derive(Debug)] 73 | pub struct Contest { 74 | /// Contest unique ID, locally generated 75 | pub id: i64, 76 | /// Contest name, unique and locally generated 77 | pub name: String, 78 | /// The prize the owner of the `chan` wants to give to the contest's winner 79 | pub prize: String, 80 | /// Contest end date and time. Invitations received after the end date 81 | /// won't generate an increase in the ranking. 82 | pub end: DateTime, 83 | /// Whenever the contest's owner decided to start the Contest 84 | pub started_at: Option>, 85 | /// True if the user decided to stop this contest. 86 | pub stopped: bool, 87 | /// The channel ID for this contest 88 | pub chan: i64, 89 | } 90 | 91 | /// Helper struct containing a rank ID and a Contest 92 | #[derive(Debug)] 93 | pub struct RankContest { 94 | /// A user rank (position) 95 | pub rank: i64, 96 | /// The contest associated 97 | pub c: Contest, 98 | } 99 | 100 | /// Rank is like a row in a ranking table. 101 | #[derive(Debug, Clone)] 102 | pub struct Rank { 103 | /// The position in the chart 104 | pub rank: i64, 105 | /// Number of invitations sent by this user 106 | pub invites: i64, 107 | /// The user that is in `rank` position because it sent `invites` invitations 108 | pub user: User, 109 | } 110 | 111 | /// Unique type for a `typemap::Key` used to fetch from the Telexide context 112 | /// the `r2d2::Pool` 113 | pub struct DBKey; 114 | impl Key for DBKey { 115 | type Value = r2d2::Pool; 116 | } 117 | 118 | /// Unique type for a `typemap::Key` used to fetch from the Telexide context 119 | /// the bot name, without accessing in this way to the `env`. 120 | pub struct NameKey; 121 | impl Key for NameKey { 122 | type Value = String; 123 | } 124 | -------------------------------------------------------------------------------- /src/telegram/messages.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use log::error; 16 | use telexide_fork::{ 17 | api::types::{AnswerCallbackQuery, DeleteMessage, SendMessage}, 18 | model::{InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, ReplyMarkup}, 19 | prelude::*, 20 | }; 21 | 22 | use crate::persistence::types::Channel; 23 | 24 | /// Sends to the `chat_id` the list of the commands. 25 | /// Used to show a raw menu to the user after the execution of any command. 26 | /// 27 | /// # Arguments 28 | /// * `ctx` - Telexide context 29 | /// * `chat_id` - The chat ID. 30 | /// 31 | /// # Panics 32 | /// Panics if Telegram returns a error. 33 | pub async fn display_main_commands(ctx: &Context, chat_id: i64) { 34 | let text = escape_markdown( 35 | "What do you want to do?\n\ 36 | /register - Register a channel/group to the bot\n\ 37 | /list - List your registered groups/channels\n\ 38 | /contest - Start/Manage the referral contest\n\ 39 | /rank - Your rank in the challenges you joined\n", 40 | None, 41 | ); 42 | let mut reply = SendMessage::new(chat_id, &text); 43 | reply.set_parse_mode(&ParseMode::MarkdownV2); 44 | let res = ctx.api.send_message(reply).await; 45 | if res.is_err() { 46 | let err = res.err().unwrap(); 47 | error!("[help] {}", err); 48 | } 49 | } 50 | 51 | /// Escape the input `text` to support Telegram Markdown V2. 52 | /// Depending on the `entity_type` changes the rules. If in `pre`,`code` or `text_link` 53 | /// there's only a small subset of characters to escape, otherwise the default pattern escapes 54 | /// almost every non ASCII character. 55 | /// 56 | /// # Arguments 57 | /// * `text`: The string slice containing the text to escape 58 | /// * `entity_type`: Optional entity type (`pre`, `code` or `text_link`). 59 | /// 60 | /// # Panics 61 | /// It panics if the regex used for the escape fails to be built. 62 | #[must_use] 63 | pub fn escape_markdown(text: &str, entity_type: Option<&str>) -> String { 64 | let mut pattern = r#"'_*[]()~`>#+-=|{}.!"#; 65 | if let Some(entity) = entity_type { 66 | pattern = match entity { 67 | "pre" | "code" => r#"\`"#, 68 | "text_link" => r#"\)"#, 69 | _ => pattern, 70 | }; 71 | } 72 | let pattern = format!("([{}])", regex::escape(pattern)); 73 | let re = regex::Regex::new(&pattern).unwrap(); 74 | return re.replace_all(text, r#"\$1"#).to_string(); 75 | } 76 | 77 | /// Deletes a message with `message_id` from `chat_id`. 78 | /// 79 | /// # Arguments 80 | /// * `ctx` - Telexide context 81 | /// * `chat_id` - The chat ID. 82 | /// * `message_id` - The ID of the message to delete. 83 | /// 84 | /// # Panics 85 | /// Panics if Telegram returns a error. 86 | pub async fn delete_message(ctx: &Context, chat_id: i64, message_id: i64) { 87 | let res = ctx 88 | .api 89 | .delete_message(DeleteMessage::new(chat_id, message_id)) 90 | .await; 91 | 92 | if res.is_err() { 93 | let err = res.err().unwrap(); 94 | error!("[delete parent message] {}", err); 95 | } 96 | } 97 | 98 | /// Display the manage menu (grid of buttons), to use when the user is 99 | /// creating/managing contests. 100 | /// 101 | /// # Arguments 102 | /// * `ctx` - Telexide context 103 | /// * `chat_id` - The chat ID. 104 | /// * `chan` - The channel that's being managed. 105 | /// 106 | /// # Panics 107 | /// Panics if Telegram returns a error. 108 | pub async fn display_manage_menu(ctx: &Context, chat_id: i64, chan: &Channel) { 109 | let mut reply = SendMessage::new( 110 | chat_id, 111 | &escape_markdown(&format!("{}\n\nWhat do you want to do?", chan.name), None), 112 | ); 113 | reply.set_parse_mode(&ParseMode::MarkdownV2); 114 | let inline_keyboard = vec![ 115 | vec![ 116 | InlineKeyboardButton { 117 | text: "\u{270d}\u{fe0f} Create".to_owned(), 118 | // start, chan 119 | callback_data: Some(format!("create {}", chan.id)), 120 | callback_game: None, 121 | login_url: None, 122 | pay: None, 123 | switch_inline_query: None, 124 | switch_inline_query_current_chat: None, 125 | url: None, 126 | }, 127 | InlineKeyboardButton { 128 | text: "\u{274c} Delete".to_owned(), 129 | callback_data: Some(format!("delete {}", chan.id)), 130 | callback_game: None, 131 | login_url: None, 132 | pay: None, 133 | switch_inline_query: None, 134 | switch_inline_query_current_chat: None, 135 | url: None, 136 | }, 137 | ], 138 | vec![ 139 | InlineKeyboardButton { 140 | text: "\u{25b6}\u{fe0f} Start".to_owned(), 141 | // start, chan 142 | callback_data: Some(format!("start {}", chan.id)), 143 | callback_game: None, 144 | login_url: None, 145 | pay: None, 146 | switch_inline_query: None, 147 | switch_inline_query_current_chat: None, 148 | url: None, 149 | }, 150 | InlineKeyboardButton { 151 | text: "\u{23f9} Stop".to_owned(), 152 | callback_data: Some(format!("stop {}", chan.id)), 153 | callback_game: None, 154 | login_url: None, 155 | pay: None, 156 | switch_inline_query: None, 157 | switch_inline_query_current_chat: None, 158 | url: None, 159 | }, 160 | ], 161 | vec![ 162 | InlineKeyboardButton { 163 | text: "\u{1f4c4}List".to_owned(), 164 | callback_data: Some(format!("list {}", chan.id)), 165 | callback_game: None, 166 | login_url: None, 167 | pay: None, 168 | switch_inline_query: None, 169 | switch_inline_query_current_chat: None, 170 | url: None, 171 | }, 172 | InlineKeyboardButton { 173 | text: "\u{1f519}Menu".to_owned(), 174 | callback_data: Some(format!("main {}", chan.id)), 175 | callback_game: None, 176 | login_url: None, 177 | pay: None, 178 | switch_inline_query: None, 179 | switch_inline_query_current_chat: None, 180 | url: None, 181 | }, 182 | ], 183 | ]; 184 | reply.set_parse_mode(&ParseMode::MarkdownV2); 185 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 186 | inline_keyboard, 187 | })); 188 | 189 | let res = ctx.api.send_message(reply).await; 190 | if res.is_err() { 191 | let err = res.err().unwrap(); 192 | error!("[manage send] {}", err); 193 | } 194 | } 195 | 196 | /// Removes the loading icon added by telegram to the user-clicked button. 197 | /// 198 | /// # Arguments 199 | /// * `ctx` - Telexide contest 200 | /// * `callback_id` - The ID that generated the loading icon to be added 201 | /// * `text` - Optional text to show, in an alert, if present. 202 | /// 203 | /// # Panics 204 | /// Panics of Telegram returns an error. 205 | pub async fn remove_loading_icon(ctx: &Context, callback_id: &str, text: Option<&str>) { 206 | let res = ctx 207 | .api 208 | .answer_callback_query(AnswerCallbackQuery { 209 | callback_query_id: callback_id.to_string(), 210 | cache_time: None, 211 | show_alert: text.is_some(), 212 | text: if text.is_some() { 213 | Some(text.unwrap().to_string()) 214 | } else { 215 | None 216 | }, 217 | url: None, 218 | }) 219 | .await; 220 | if res.is_err() { 221 | error!("[remove_loading_icon] {}", res.err().unwrap()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/telegram/channels.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use log::{error, info}; 16 | use rusqlite::params; 17 | use telexide_fork::{ 18 | api::types::{CreateChatInviteLink, GetChat, GetChatAdministrators, SendMessage}, 19 | model::{AdministratorMemberStatus, Chat, ChatMember}, 20 | prelude::*, 21 | }; 22 | 23 | use crate::persistence::types::{Channel, DBKey}; 24 | 25 | /// Returns all the channels owned by `user_id`. 26 | /// 27 | /// # Arguments: 28 | /// * `ctx` - Telexide `Context` 29 | /// * `user_id` - The user ID 30 | /// 31 | /// # Panics 32 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 33 | #[must_use] 34 | pub fn get_all(ctx: &Context, user_id: i64) -> Vec { 35 | let guard = ctx.data.read(); 36 | let map = guard.get::().expect("db"); 37 | let conn = map.get().unwrap(); 38 | let mut stmt = conn 39 | .prepare("SELECT id, link, name FROM channels WHERE registered_by = ? ORDER BY id ASC") 40 | .unwrap(); 41 | 42 | let channels = stmt 43 | .query_map(params![user_id], |row| { 44 | Ok(Channel { 45 | id: row.get(0)?, 46 | registered_by: user_id, 47 | link: row.get(1)?, 48 | name: row.get(2)?, 49 | }) 50 | }) 51 | .unwrap() 52 | .map(Result::unwrap) 53 | .collect(); 54 | channels 55 | } 56 | 57 | /// Returns all the admins of the `chat_id`. In case of errors sends a message to the `user_id` 58 | /// and logs with `error!`. 59 | /// 60 | /// # Arguments 61 | /// * `ctx` - Telexide context 62 | /// * `chat_id` - The unique id of the group/chan under examination 63 | /// * `user_id` - The user that requested this admin list. 64 | /// 65 | /// # Panics 66 | /// Panics if the Telegram server returns error. 67 | pub async fn admins(ctx: &Context, chat_id: i64, user_id: i64) -> Vec { 68 | let admins = ctx 69 | .api 70 | .get_chat_administrators(GetChatAdministrators { chat_id }) 71 | .await; 72 | if admins.is_err() { 73 | let res = ctx 74 | .api 75 | .send_message(SendMessage::new( 76 | user_id, 77 | "Error! You must add this bot as admin of the group/channel.", 78 | )) 79 | .await; 80 | if res.is_err() { 81 | let err = res.err().unwrap(); 82 | error!("[register] send message {}", err); 83 | } 84 | return vec![]; 85 | } 86 | let admins = admins.unwrap(); 87 | 88 | admins 89 | .iter() 90 | .filter_map(|u| { 91 | if let ChatMember::Administrator(admin) = u { 92 | Some(admin.clone()) 93 | } else { 94 | None 95 | } 96 | }) 97 | .collect() 98 | } 99 | 100 | /// Tries to register a chat identified by its `chat_id`. The chat can be 101 | /// - a channel 102 | /// - a group 103 | /// - a supergroup 104 | /// 105 | /// Returns true in case of registration success. 106 | /// 107 | /// # Arguments 108 | /// * `ctx` - Telexide context 109 | /// * `chat_id` - Unique identifier of the chat 110 | /// * `registered_by` - Unique identifier of the `User` (`user.id`) that wants to register the 111 | /// chat. 112 | /// 113 | /// # Panics 114 | /// 115 | /// Panics if the commincation with telegram fails, or if the database is failing. 116 | pub async fn try_register(ctx: &Context, chat_id: i64, registered_by: i64) -> bool { 117 | // NOTE: we need this get_chat call because chat.invite_link is returned only by 118 | // calling GetChat: https://core.telegram.org/bots/api#chat 119 | info!("try_register begin"); 120 | let (mut invite_link, username, title, is_channel) = { 121 | match ctx.api.get_chat(GetChat { chat_id }).await.unwrap() { 122 | Chat::Channel(c) => (c.invite_link, c.username, Some(c.title), true), 123 | Chat::Group(c) => (c.invite_link, c.username, Some(c.title), false), 124 | Chat::SuperGroup(c) => (c.invite_link, c.username, Some(c.title), false), 125 | Chat::Private(_) => (None, None, None, false), 126 | } 127 | }; 128 | 129 | if invite_link.is_none() && username.is_none() { 130 | // Try to generate, it might happen (?) anyway is safe hence who cares 131 | if let Ok(invite) = ctx 132 | .api 133 | .create_chat_invite_link(CreateChatInviteLink { 134 | chat_id, 135 | expire_date: None, 136 | member_limit: None, 137 | }) 138 | .await 139 | { 140 | invite_link = Some(invite.invite_link); 141 | } 142 | } 143 | 144 | let link: String = { 145 | if let Some(invite_link) = invite_link { 146 | invite_link.to_string() 147 | } else if let Some(username) = username { 148 | format!("https://t.me/{username}") 149 | } else { 150 | String::new() 151 | } 152 | }; 153 | 154 | if link.is_empty() { 155 | // INFO: info is correct since if we are not able to extract these information 156 | // perhaps the received message is not from a chan/group/supergroup and 157 | // there's no need to send anything back to the user 158 | info!( 159 | "[register] Unable to extract invite link / username for {}", 160 | chat_id 161 | ); 162 | return false; 163 | } 164 | 165 | let admins = admins(ctx, chat_id, registered_by).await; 166 | let mut found = false; 167 | let me = ctx.api.get_me().await.unwrap(); // the bot! 168 | for admin in admins { 169 | // permissions in channel and groups are a bit different 170 | if admin.user.is_bot 171 | && admin.user.id == me.id 172 | && admin.can_manage_chat 173 | && ((is_channel 174 | && admin.can_post_messages.is_some() 175 | && admin.can_post_messages.unwrap()) 176 | || (!is_channel 177 | && admin.can_pin_messages.is_some() 178 | && admin.can_pin_messages.unwrap())) 179 | { 180 | found = true; 181 | break; 182 | } 183 | } 184 | 185 | if !found { 186 | let res = ctx 187 | .api 188 | .send_message(SendMessage::new( 189 | registered_by, 190 | "The bot must be admin of the channel/group, and shall be able to:\n\n\ 191 | 1. manage the chat.\n2. post messages\n3. pin messages", 192 | )) 193 | .await; 194 | if res.is_err() { 195 | let err = res.err().unwrap(); 196 | error!("[register] send message {}", err); 197 | } 198 | return false; 199 | } 200 | 201 | let title = title.unwrap(); 202 | let res = { 203 | let guard = ctx.data.read(); 204 | let map = guard.get::().expect("db"); 205 | let conn = map.get().unwrap(); 206 | 207 | conn.execute( 208 | "INSERT OR IGNORE INTO channels(id, registered_by, link, name) VALUES(?, ?, ?, ?)", 209 | params![chat_id, registered_by, link, title], 210 | ) 211 | }; 212 | 213 | if res.is_err() { 214 | let err = res.err().unwrap(); 215 | error!("[register] {}", err); 216 | 217 | let res = ctx 218 | .api 219 | .send_message(SendMessage::new(registered_by, &err.to_string())) 220 | .await; 221 | if res.is_err() { 222 | let err = res.err().unwrap(); 223 | error!("[register] send message {}", err); 224 | } 225 | return false; 226 | } 227 | 228 | // from here below, the registration is succeded, hence if we fail in deliver a 229 | // message we dont' return false, because in the DB is all OK 230 | let res = ctx 231 | .api 232 | .send_message(SendMessage::new( 233 | registered_by, 234 | &format!("Channel/Group {title} registered succesfully!"), 235 | )) 236 | .await; 237 | 238 | if res.is_err() { 239 | let err = res.err().unwrap(); 240 | error!("[final register] {}", err); 241 | } 242 | info!("try_register end"); 243 | true 244 | } 245 | -------------------------------------------------------------------------------- /src/telegram/contests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use chrono::{DateTime, Utc}; 16 | use log::error; 17 | use rusqlite::params; 18 | use telexide_fork::{api::types::GetChatMember, prelude::*}; 19 | 20 | use crate::persistence::types::{Contest, DBKey, Rank}; 21 | use crate::telegram::users; 22 | 23 | use std::string::ToString; 24 | 25 | /// Returns the `Contest` with the specified `id`, if exists. 26 | /// 27 | /// # Arguments 28 | /// * `ctx` - Telexide context 29 | /// * `id` - The ID (`RaF` generated) of the contest to search. 30 | /// 31 | /// # Panics 32 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 33 | #[must_use] 34 | pub fn get(ctx: &Context, id: i64) -> Option { 35 | let guard = ctx.data.read(); 36 | let map = guard.get::().expect("db"); 37 | let conn = map.get().unwrap(); 38 | let mut stmt = conn 39 | .prepare("SELECT name, prize, end, started_at, chan, stopped FROM contests WHERE id = ?") 40 | .unwrap(); 41 | let mut iter = stmt 42 | .query_map(params![id], |row| { 43 | Ok(Contest { 44 | id, 45 | name: row.get(0)?, 46 | prize: row.get(1)?, 47 | end: row.get(2)?, 48 | started_at: row.get(3)?, 49 | chan: row.get(4)?, 50 | stopped: row.get(5)?, 51 | }) 52 | }) 53 | .unwrap(); 54 | let c = iter.next().unwrap(); 55 | if let Ok(c) = c { 56 | return Some(c); 57 | } 58 | None 59 | } 60 | 61 | /// Returns all the `Contest` created for the channel with ID `id`. 62 | /// 63 | /// # Arguments 64 | /// * `ctx` - Telexide context 65 | /// * `chan` - The ID (Telegram generated) of the Channel. 66 | /// 67 | /// # Panics 68 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 69 | #[must_use] 70 | pub fn get_all(ctx: &Context, chan: i64) -> Vec { 71 | let guard = ctx.data.read(); 72 | let map = guard.get::().expect("db"); 73 | let conn = map.get().unwrap(); 74 | let mut stmt = conn 75 | .prepare( 76 | "SELECT id, name, prize, end, started_at, stopped FROM contests WHERE chan = ? ORDER BY end DESC", 77 | ) 78 | .unwrap(); 79 | 80 | let contests = stmt 81 | .query_map(params![chan], |row| { 82 | Ok(Contest { 83 | id: row.get(0)?, 84 | name: row.get(1)?, 85 | prize: row.get(2)?, 86 | end: row.get(3)?, 87 | started_at: row.get(4)?, 88 | stopped: row.get(5)?, 89 | chan, 90 | }) 91 | }) 92 | .unwrap() 93 | .map(std::result::Result::unwrap) 94 | .collect(); 95 | contests 96 | } 97 | 98 | /// Returns rank for the `contest`, already oredered by number of invites accepted in descending 99 | /// order. 100 | /// 101 | /// # Arguments 102 | /// * `ctx` - Telexide context 103 | /// * `contest` - The `Contest` under examination 104 | /// 105 | /// # Panics 106 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 107 | #[must_use] 108 | pub fn ranking(ctx: &Context, contest: &Contest) -> Vec { 109 | let guard = ctx.data.read(); 110 | let map = guard.get::().expect("db"); 111 | let conn = map.get().unwrap(); 112 | // NOTE: the ordering ALSO via t.source is required to give a meaningful order (depending on 113 | // the id, hence jsut to have them different) in case of equal rank 114 | let mut stmt = conn 115 | .prepare( 116 | "SELECT ROW_NUMBER() OVER (ORDER BY t.c, t.source DESC) AS r, t.c, t.source 117 | FROM (SELECT COUNT(*) AS c, source FROM invitations WHERE contest = ? GROUP BY source) AS t", 118 | ) 119 | .unwrap(); 120 | stmt.query_map(params![contest.id], |row| { 121 | Ok(Rank { 122 | rank: row.get(0)?, 123 | invites: row.get(1)?, 124 | user: users::get(ctx, row.get(2)?).unwrap(), 125 | }) 126 | }) 127 | .unwrap() 128 | .map(std::result::Result::unwrap) 129 | .collect::>() 130 | } 131 | 132 | /// Possible errors while creating a Contest 133 | #[derive(Debug, Clone)] 134 | pub enum Error { 135 | /// Error while parsing the user inserted date 136 | ParseError(chrono::format::ParseError), 137 | /// Generic error we want to report to the user as a string 138 | GenericError(String), 139 | } 140 | 141 | impl From for Error { 142 | /// Returns `Error::ParseError` 143 | fn from(error: chrono::format::ParseError) -> Error { 144 | Error::ParseError(error) 145 | } 146 | } 147 | 148 | impl From for Error { 149 | /// Returns `Error::GenericError` 150 | fn from(error: String) -> Error { 151 | Error::GenericError(error) 152 | } 153 | } 154 | 155 | impl std::fmt::Display for Error { 156 | /// Format all the possible errors 157 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 158 | match self { 159 | Error::ParseError(error) => write!(f, "DateTime parse {error}"), 160 | Error::GenericError(error) => write!(f, "{error}"), 161 | } 162 | } 163 | } 164 | 165 | /// Parse the input `text` and creates a valid `Contest` associated to the chan. 166 | /// 167 | /// # Arguments 168 | /// 169 | /// * `text` - A string slice holding the user inserted text 170 | /// * `chan` - The channel to associate with the Contest in case of success 171 | /// 172 | /// # Errors 173 | /// If the parsing from text fails for whatever reason, it returns an `Error` 174 | /// that contains a detail. In case of failed parsing, it's a `Error::ParseError(e)` 175 | /// otherwise is a `Error::GenericError(s)` with a string containing the reason 176 | /// of the failure. 177 | pub fn from_text(text: &str, chan: i64) -> Result { 178 | let rows = text 179 | .split('\n') 180 | .skip_while(|r| r.is_empty()) 181 | .collect::>(); 182 | if rows.len() != 3 { 183 | return Err(format!("failed because row.len() != 3. Got: {}", rows.len()).into()); 184 | } 185 | let id = -1; 186 | let name = rows[0].to_string(); 187 | let prize = rows[2].to_string(); 188 | // user input: YYYY-MM-DD hh:mm TZ, needs to become 189 | // YYYY-MM-DD hh:mm:ss TZ to get enough data to create a datetime object 190 | let add_seconds = |row: &str| -> String { 191 | let mut elements = row 192 | .split_whitespace() 193 | .map(ToString::to_string) 194 | .collect::>(); 195 | if elements.len() != 3 { 196 | return row.to_string(); 197 | } 198 | // 0: YYYY-MM-DD 199 | // 1: hh:mm 200 | // 2: TZ 201 | elements[1] += ":00"; 202 | elements.join(" ") 203 | }; 204 | let now = Utc::now(); 205 | let end: DateTime = 206 | DateTime::parse_from_str(&add_seconds(rows[1]), "%Y-%m-%d %H:%M:%S %#z")?.into(); 207 | if end < now { 208 | return Err("End date can't be in the past".to_string().into()); 209 | } 210 | Ok(Contest { 211 | id, 212 | end, 213 | name, 214 | prize, 215 | chan, 216 | stopped: false, 217 | started_at: None, 218 | }) 219 | } 220 | 221 | /// Count the users that participated to the `contest` 222 | /// 223 | /// # Arguments 224 | /// 225 | /// * `ctx`: The telexide ctx, used to get the db 226 | /// * `contest`: The Contest under examination 227 | /// 228 | /// # Panics 229 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 230 | #[must_use] 231 | pub fn count_users(ctx: &Context, contest: &Contest) -> i64 { 232 | struct Counter { 233 | value: i64, 234 | } 235 | let guard = ctx.data.read(); 236 | let map = guard.get::().expect("db"); 237 | let conn = map.get().unwrap(); 238 | let mut stmt = conn 239 | .prepare("SELECT COUNT(id) FROM invitations WHERE contest = ?") 240 | .unwrap(); 241 | let vals = stmt 242 | .query_map(params![contest.id], |row| { 243 | Ok(Counter { value: row.get(0)? }) 244 | }) 245 | .unwrap() 246 | .map(|count| count.unwrap_or(Counter { value: -1 }).value) 247 | .collect::>(); 248 | if vals.is_empty() { 249 | return 0; 250 | } 251 | vals[0] 252 | } 253 | 254 | /// Function to call to verify that the joined users are still in the channel. 255 | /// NOTE: this function is async because it uses the async `ctx.api.get_chat_member` 256 | /// function to check if the user is still inside the channel referenced by the `contest`. 257 | /// 258 | /// # Arguments 259 | /// * `ctx`: The Telexide context, used to get the db 260 | /// * `contest`: The Contest under examination 261 | /// 262 | /// # Panics 263 | /// Panics if the connection to the DB fails, or if the returned data is corrupt. 264 | pub async fn validate_users(ctx: &Context, contest: &Contest) { 265 | struct InnerUser { 266 | id: i64, 267 | } 268 | let users = { 269 | let guard = ctx.data.read(); 270 | let map = guard.get::().expect("db"); 271 | let conn = map.get().unwrap(); 272 | let mut stmt = conn 273 | .prepare("SELECT dest FROM invitations WHERE contest = ?") 274 | .unwrap(); 275 | stmt.query_map(params![contest.id], |row| Ok(InnerUser { id: row.get(0)? })) 276 | .unwrap() 277 | .map(|user| user.unwrap().id) 278 | .collect::>() 279 | }; 280 | 281 | for user in users { 282 | let member = ctx 283 | .api 284 | .get_chat_member(GetChatMember { 285 | chat_id: contest.chan, 286 | user_id: user, 287 | }) 288 | .await; 289 | 290 | let in_channel = member.is_ok(); 291 | if !in_channel { 292 | let res = { 293 | let guard = ctx.data.read(); 294 | let map = guard.get::().expect("db"); 295 | let conn = map.get().unwrap(); 296 | let mut stmt = conn 297 | .prepare("DELETE FROM invitations WHERE dest = ? and contest = ?") 298 | .unwrap(); 299 | stmt.execute(params![user, contest.id]) 300 | }; 301 | if res.is_err() { 302 | error!("[users validation] {}", res.err().unwrap()); 303 | } 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/telegram/commands.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use data_encoding::BASE64URL; 16 | use log::{error, info}; 17 | use rusqlite::params; 18 | use std::collections::HashMap; 19 | 20 | use telexide_fork::{ 21 | api::types::SendMessage, 22 | framework::{CommandError, CommandResult}, 23 | model::{InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, ReplyMarkup}, 24 | prelude::*, 25 | }; 26 | 27 | use crate::{ 28 | persistence::types::{Channel, DBKey, NameKey, RankContest}, 29 | telegram::{ 30 | channels, contests, 31 | messages::{display_main_commands, escape_markdown}, 32 | users, 33 | }, 34 | }; 35 | 36 | /// Rank command. Shows to the user his/her rank for every joined challenge. 37 | /// 38 | /// # Arguments 39 | /// * `ctx` - Telexide context 40 | /// * `message` - Received message with the command inside 41 | /// 42 | /// # Panics 43 | /// Panics if the connection to the db fails, or if telegram servers return error. 44 | #[command(description = "Your rank in the challenges you joined")] 45 | pub async fn rank(ctx: Context, message: Message) -> CommandResult { 46 | info!("rank command begin"); 47 | let sender_id = message.from.clone().unwrap().id; 48 | let rank_per_user_contest = { 49 | let guard = ctx.data.read(); 50 | let map = guard.get::().expect("db"); 51 | let conn = map.get().unwrap(); 52 | let mut stmt = conn 53 | .prepare( 54 | "SELECT ROW_NUMBER() OVER (ORDER BY t.c, t.source DESC) AS r, t.contest 55 | FROM (SELECT COUNT(*) AS c, contest, source FROM invitations GROUP BY contest, source) AS t 56 | WHERE t.source = ?", 57 | ) 58 | .unwrap(); 59 | 60 | let mut iter = stmt 61 | .query_map(params![sender_id], |row| { 62 | Ok(RankContest { 63 | rank: row.get(0)?, 64 | c: contests::get(&ctx, row.get(1)?).unwrap(), 65 | }) 66 | }) 67 | .unwrap() 68 | .peekable(); 69 | if iter.peek().is_some() && iter.peek().unwrap().is_ok() { 70 | iter.map(std::result::Result::unwrap) 71 | .collect::>() 72 | } else { 73 | vec![] 74 | } 75 | }; 76 | 77 | let text = if rank_per_user_contest.is_empty() { 78 | "You haven't participated in any contest yet!".to_string() 79 | } else { 80 | let mut m = "Your rankings\n\n".to_string(); 81 | for rank_contest in rank_per_user_contest { 82 | let c = rank_contest.c; 83 | let rank = rank_contest.rank; 84 | m += &format!("Contest \"{}({})\": ", c.name, c.end); 85 | if rank == 1 { 86 | m += "\u{1f947}#1!"; 87 | } else if rank <= 3 { 88 | m += &format!("\u{1f3c6} #{rank}"); 89 | } else { 90 | m += &format!("#{rank}"); 91 | } 92 | m += "\n"; 93 | } 94 | m 95 | }; 96 | let mut reply = SendMessage::new(sender_id, &escape_markdown(&text, None)); 97 | reply.set_parse_mode(&ParseMode::MarkdownV2); 98 | let res = ctx.api.send_message(reply).await; 99 | if res.is_err() { 100 | let err = res.err().unwrap(); 101 | error!("[rank] {}", err); 102 | } 103 | 104 | display_main_commands(&ctx, sender_id).await; 105 | info!("rank command end"); 106 | Ok(()) 107 | } 108 | 109 | /// Help command. Shows to the user the help menu with the complete command list. 110 | /// 111 | /// # Arguments 112 | /// * `ctx` - Telexide context 113 | /// * `message` - Received message with the command inside 114 | #[command(description = "Help menu")] 115 | pub async fn help(ctx: Context, message: Message) -> CommandResult { 116 | info!("help command begin"); 117 | let sender_id = message.from.clone().unwrap().id; 118 | let text = escape_markdown( 119 | "I can create contests based on the referral strategy. \ 120 | The user that referes more (legit) users will win a prize!\n\n\ 121 | You can control me by sending these commands:\n\n\ 122 | /register - Register a channel/group to the bot\n\ 123 | /list - List your registered groups/channels\n\ 124 | /contest - Start/Manage the referral contest\n\ 125 | /rank - Your rank in the challenges you joined\n\ 126 | /help - This menu", 127 | None, 128 | ); 129 | let mut reply = SendMessage::new(sender_id, &text); 130 | reply.set_parse_mode(&ParseMode::MarkdownV2); 131 | let res = ctx.api.send_message(reply).await; 132 | if res.is_err() { 133 | let err = res.err().unwrap(); 134 | error!("[help] {}", err); 135 | } 136 | info!("help command end"); 137 | Ok(()) 138 | } 139 | 140 | /// Contest command. Start/Manage the referral contest. 141 | /// 142 | /// # Arguments 143 | /// * `ctx` - Telexide context 144 | /// * `message` - Received message with the commands inside 145 | /// 146 | /// # Panics 147 | /// Panics if the connection to the db fails, or if telegram servers return error. 148 | #[command(description = "Start/Manage the referral contest")] 149 | pub async fn contest(ctx: Context, message: Message) -> CommandResult { 150 | info!("contest command begin"); 151 | let sender_id = message.from.clone().unwrap().id; 152 | let channels = channels::get_all(&ctx, sender_id); 153 | 154 | if channels.is_empty() { 155 | let reply = SendMessage::new(sender_id, "You have no registered groups/channels!"); 156 | let res = ctx.api.send_message(reply).await; 157 | if res.is_err() { 158 | let err = res.err().unwrap(); 159 | error!("[list channels] {}", err); 160 | } 161 | display_main_commands(&ctx, sender_id).await; 162 | } else { 163 | let mut reply = SendMessage::new(sender_id, "Select the group/channel you want to manage"); 164 | 165 | let mut partition_size: usize = channels.len() / 2; 166 | if partition_size < 2 { 167 | partition_size = 1; 168 | } 169 | let inline_keyboard: Vec> = channels 170 | .chunks(partition_size) 171 | .map(|chunk| { 172 | chunk 173 | .iter() 174 | .map(|channel| InlineKeyboardButton { 175 | text: channel.name.clone(), 176 | // manage, channel id 177 | callback_data: Some(format!("manage {}", channel.id)), 178 | callback_game: None, 179 | login_url: None, 180 | pay: None, 181 | switch_inline_query: None, 182 | switch_inline_query_current_chat: None, 183 | url: None, 184 | }) 185 | .collect() 186 | }) 187 | .collect(); 188 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 189 | inline_keyboard, 190 | })); 191 | reply.set_parse_mode(&ParseMode::MarkdownV2); 192 | let res = ctx.api.send_message(reply).await; 193 | if res.is_err() { 194 | let err = res.err().unwrap(); 195 | error!("[list channels] {}", err); 196 | } 197 | } 198 | 199 | info!("contest command end"); 200 | Ok(()) 201 | } 202 | 203 | /// Start command. Depending on the `message` content executes different actions. 204 | /// In any case, it adds the users to the list of the known users. 205 | /// 206 | /// - If the message contains only `/start` it starts the bot with an hello message. 207 | /// - If the message contains the base64 encoded parameters: user, channel, contest 208 | /// this is an invitation link from user, to join the channel, because of contest. 209 | /// - If the message contains the base64 encoded parameters: channel, contest 210 | /// this is the link `RaF` generated and posted to the channel, that ever partecipant uses to 211 | /// generate its own referral link. 212 | /// 213 | /// # Arguments 214 | /// * `ctx` - Telexide context 215 | /// * `message` - Received message with the commands inside 216 | /// 217 | /// # Panics 218 | /// Panics if the connection to the db fails, or if telegram servers return error. 219 | #[command(description = "Start the Bot")] 220 | pub async fn start(ctx: Context, message: Message) -> CommandResult { 221 | info!("start command begin"); 222 | let sender_id = message.from.clone().unwrap().id; 223 | // We should also check that at that time the user is not inside the chan 224 | // and that it comes to the channel only by following this custom link 225 | // with all the process (referred -> what channel? -> click in @channel 226 | // (directly from the bot, hence save the chan name) -> joined 227 | // Once done, check if it's inside (and save the date). 228 | 229 | // On start, save the user ID if not already present 230 | let res = { 231 | let guard = ctx.data.read(); 232 | let map = guard.get::().expect("db"); 233 | let conn = map.get().unwrap(); 234 | let user = message.from.clone().unwrap(); 235 | 236 | conn.execute( 237 | "INSERT OR IGNORE INTO users(id, first_name, last_name, username) VALUES(?, ?, ?, ?)", 238 | params![user.id, user.first_name, user.last_name, user.username,], 239 | ) 240 | }; 241 | if res.is_err() { 242 | let err = res.err().unwrap(); 243 | error!("[insert user] {}", err); 244 | ctx.api 245 | .send_message(SendMessage::new(sender_id, &format!("[insert user] {err}"))) 246 | .await?; 247 | } 248 | 249 | // ?start=base64encode(source=&chan=) 250 | // message = "start base64encode(source=ecc)" 251 | // source AND chan == invitation 252 | // chan ALONE = sent by the bot inside the chan, we have to generate the referring link 253 | // (encode with source = current user and this chan) that he can use to share the invite 254 | let text = message.get_text().unwrap(); 255 | let mut split = text.split_ascii_whitespace(); 256 | split.next(); // /start 257 | if let Some(encoded_params) = split.next() { 258 | let params = BASE64URL.decode(encoded_params.as_bytes())?; 259 | let params: HashMap<_, _> = url::form_urlencoded::parse(params.as_slice()).collect(); 260 | info!("start params decoded: {:?}", params); 261 | 262 | let source = if params.contains_key("source") { 263 | params["source"].parse::().unwrap_or(-1) 264 | } else { 265 | -1 266 | }; 267 | let chan = if params.contains_key("chan") { 268 | params["chan"].parse::().unwrap_or(-1) 269 | } else { 270 | -1 271 | }; 272 | let contest_id = if params.contains_key("contest") { 273 | params["contest"].parse::().unwrap_or(-1) 274 | } else { 275 | -1 276 | }; 277 | 278 | let (user, channel, c) = { 279 | let guard = ctx.data.read(); 280 | let map = guard.get::().expect("db"); 281 | let conn = map.get().unwrap(); 282 | let mut stmt = conn 283 | .prepare("SELECT link, name, registered_by FROM channels WHERE id = ?") 284 | .unwrap(); 285 | 286 | let channel = stmt 287 | .query_map(params![chan], |row| { 288 | Ok(Channel { 289 | id: chan, 290 | link: row.get(0)?, 291 | name: row.get(1)?, 292 | registered_by: row.get(2)?, 293 | }) 294 | }) 295 | .unwrap() 296 | .map(std::result::Result::unwrap) 297 | .next(); 298 | 299 | let user = users::get(&ctx, source); 300 | let c = contests::get(&ctx, contest_id); 301 | (user, channel, c) 302 | }; 303 | 304 | // Error 305 | if user.is_none() && channel.is_none() { 306 | ctx.api 307 | .send_message(SendMessage::new( 308 | sender_id, 309 | "Something wrong with the group/channel or the user that's inviting you.\n\ 310 | Contact the support.", 311 | )) 312 | .await?; 313 | return Err(CommandError( 314 | "Something wrong with the group/channel or the user that's inviting you".to_owned(), 315 | )); 316 | // Invite 317 | } else if user.is_some() && channel.is_some() && c.is_some() { 318 | let user = user.unwrap(); 319 | let channel = channel.unwrap(); 320 | let c = c.unwrap(); 321 | 322 | let mut reply = SendMessage::new( 323 | sender_id, 324 | &escape_markdown( 325 | &format!( 326 | "{}{}{} invited you to join {}", 327 | user.first_name, 328 | match user.last_name { 329 | Some(last_name) => format!(" {last_name}"), 330 | None => String::new(), 331 | }, 332 | match user.username { 333 | Some(username) => format!(" (@{username})"), 334 | None => String::new(), 335 | }, 336 | channel.name 337 | ), 338 | None, 339 | ), 340 | ); 341 | 342 | let inline_keyboard = vec![vec![ 343 | InlineKeyboardButton { 344 | text: "Accept \u{2705}".to_owned(), 345 | // tick, source, dest, chan 346 | callback_data: Some(format!( 347 | "\u{2705} {} {} {} {}", 348 | user.id, 349 | message.from.clone().unwrap().id, 350 | channel.id, 351 | c.id, 352 | )), 353 | callback_game: None, 354 | login_url: None, 355 | pay: None, 356 | switch_inline_query: None, 357 | switch_inline_query_current_chat: None, 358 | url: None, 359 | }, 360 | InlineKeyboardButton { 361 | text: "Refuse \u{274c}".to_owned(), 362 | callback_data: Some("\u{274c}".to_string()), 363 | callback_game: None, 364 | login_url: None, 365 | pay: None, 366 | switch_inline_query: None, 367 | switch_inline_query_current_chat: None, 368 | url: None, 369 | }, 370 | ]]; 371 | reply.set_parse_mode(&ParseMode::MarkdownV2); 372 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 373 | inline_keyboard, 374 | })); 375 | ctx.api.send_message(reply).await?; 376 | 377 | // Bot generated url: generate invite url for current user 378 | } else if user.is_none() && channel.is_some() && c.is_some() { 379 | let chan = channel.unwrap(); 380 | let c = c.unwrap(); 381 | let bot_name = { 382 | let guard = ctx.data.read(); 383 | guard 384 | .get::() 385 | .expect("name") 386 | .clone() 387 | .replace('@', "") 388 | }; 389 | let params = BASE64URL.encode( 390 | format!( 391 | "chan={}&contest={}&source={}", 392 | chan.id, 393 | c.id, 394 | message.from.unwrap().id 395 | ) 396 | .as_bytes(), 397 | ); 398 | let invite_link = format!("https://t.me/{bot_name}?start={params}"); 399 | 400 | let text = &escape_markdown( 401 | &format!( 402 | "Thank you for joining the {contest_name} contest!\n\ 403 | Here's the link to use for inviting your friends to join {chan_name}:\n\n\ 404 | \u{1f449}\u{1f3fb}{invite_link}", 405 | contest_name = c.name, 406 | chan_name = chan.name, 407 | invite_link = invite_link 408 | ), 409 | None, 410 | ); 411 | let mut reply = SendMessage::new(sender_id, text); 412 | reply.set_parse_mode(&ParseMode::MarkdownV2); 413 | ctx.api.send_message(reply).await?; 414 | } 415 | } else { 416 | // Case in which no parameter are present 417 | 418 | // If this is a start from a group/supergroup, then we can register the group 419 | // as a channel. If instead is a start from inside the bot chat, we just say hello. 420 | let chat_id = message.chat.get_id(); 421 | let registered = channels::try_register(&ctx, chat_id, sender_id).await; 422 | if registered { 423 | display_main_commands(&ctx, sender_id).await; 424 | } else { 425 | ctx 426 | .api 427 | .send_message(SendMessage::new( 428 | sender_id, 429 | "Welcome to RaF (Refer a Friend) Bot! Have a look at the command list, with /help", 430 | )) 431 | .await?; 432 | } 433 | } 434 | 435 | info!("start command end"); 436 | Ok(()) 437 | } 438 | 439 | /// Register command. Shows to the user the procedure to register a channel/group to `RaF`. 440 | /// 441 | /// # Arguments 442 | /// * `ctx` - Telexide context 443 | /// * `message` - Received message with the commands inside 444 | /// 445 | /// # Panics 446 | /// If telegram servers return error. 447 | #[command(description = "Register your group/channel to the bot")] 448 | pub async fn register(ctx: Context, message: Message) -> CommandResult { 449 | info!("register command begin"); 450 | let sender_id = message.from.clone().unwrap().id; 451 | ctx.api 452 | .send_message(SendMessage::new( 453 | sender_id, 454 | "To register a channel to RaF\n\n\ 455 | 1) Add the bot as admin in your channel\n\ 456 | 2) Forward a message from your channel to complete the registartion\n\n\ 457 | To register a group/supergroup to RaF:\n\n\ 458 | 1) Add the bot as admin in your group/supergroup\n\ 459 | 2) Start the bot inside the group/supergroup\n\n\ 460 | That's it.", 461 | )) 462 | .await?; 463 | display_main_commands(&ctx, sender_id).await; 464 | info!("register command end"); 465 | Ok(()) 466 | } 467 | 468 | /// List command. Shows to the user the channels/groups registered 469 | /// 470 | /// # Arguments 471 | /// * `ctx` - Telexide context 472 | /// * `message` - Received message with the commands inside 473 | /// 474 | /// # Panics 475 | /// Panics if the connection to the db fails, or if telegram servers return error. 476 | #[command(description = "List your registered channels/groups")] 477 | pub async fn list(ctx: Context, message: Message) -> CommandResult { 478 | info!("list command begin"); 479 | let sender_id = message.from.clone().unwrap().id; 480 | let text = { 481 | let channels = channels::get_all(&ctx, sender_id); 482 | 483 | let mut text: String = String::new(); 484 | for (i, chan) in channels.iter().enumerate() { 485 | text += &format!( 486 | "{} [{}]({})\n", 487 | escape_markdown(&format!("{}.", i + 1), None), 488 | escape_markdown(&chan.name, None), 489 | chan.link 490 | ); 491 | } 492 | if text.is_empty() { 493 | escape_markdown("You don't have any channel registered, yet!", None) 494 | } else { 495 | text 496 | } 497 | }; 498 | 499 | let mut reply = SendMessage::new(sender_id, &text); 500 | reply.set_parse_mode(&ParseMode::MarkdownV2); 501 | 502 | let res = ctx.api.send_message(reply).await; 503 | 504 | if res.is_err() { 505 | let err = res.err().unwrap(); 506 | error!("[list channels] {}", err); 507 | } 508 | display_main_commands(&ctx, sender_id).await; 509 | 510 | info!("list command exit"); 511 | Ok(()) 512 | } 513 | 514 | /// Broadcast command. Available only for the bot owner when started with the broadcast flag. 515 | /// 516 | /// # Arguments 517 | /// * `ctx` - Telexide context 518 | /// * `message` - Received message with the command inside 519 | #[command(description = "Broadcast a message to all users and channels")] 520 | pub async fn broadcast(ctx: Context, message: Message) -> CommandResult { 521 | info!("broadcast command begin"); 522 | let everyone = { 523 | let guard = ctx.data.read(); 524 | let map = guard.get::().expect("db"); 525 | let conn = map.get().unwrap(); 526 | let mut stmt = conn 527 | .prepare("SELECT users.id from users union select channels.id from channels") 528 | .unwrap(); 529 | 530 | let mut iter = stmt 531 | .query_map(params![], |row| Ok(row.get(0)?)) 532 | .unwrap() 533 | .peekable(); 534 | if iter.peek().is_some() && iter.peek().unwrap().is_ok() { 535 | iter.map(std::result::Result::unwrap).collect::>() 536 | } else { 537 | vec![] 538 | } 539 | }; 540 | info!("sending broadcast to {} users", everyone.len()); 541 | 542 | // read text from file 543 | let text = std::fs::read_to_string("broadcast.md").unwrap(); 544 | 545 | for id in everyone { 546 | info!("sending to {}", id); 547 | let mut reply = SendMessage::new(id, &text); 548 | reply.set_parse_mode(&ParseMode::MarkdownV2); 549 | let res = ctx.api.send_message(reply).await; 550 | if res.is_err() { 551 | let err = res.err().unwrap(); 552 | error!("[broadcast to {}] {}", id, err); 553 | } 554 | } 555 | 556 | let sender_id = message.from.clone().unwrap().id; 557 | let mut reply = SendMessage::new(sender_id, &text); 558 | reply.set_parse_mode(&ParseMode::MarkdownV2); 559 | let res = ctx.api.send_message(reply).await; 560 | if res.is_err() { 561 | let err = res.err().unwrap(); 562 | error!("[broadcast] {}", err); 563 | } 564 | info!("broadcast command end"); 565 | Ok(()) 566 | } 567 | -------------------------------------------------------------------------------- /src/telegram/handlers.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Paolo Galeone 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use chrono::{DateTime, Utc}; 16 | use data_encoding::BASE64URL; 17 | use log::{error, info}; 18 | use rusqlite::params; 19 | use tabular::{Row, Table}; 20 | use telexide_fork::model::{ 21 | Chat, ChatMember, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, ReplyMarkup, 22 | UpdateContent, 23 | }; 24 | use telexide_fork::{ 25 | api::types::{AnswerCallbackQuery, GetChatMember, PinChatMessage, SendMessage}, 26 | prelude::*, 27 | }; 28 | use tokio::time::{sleep, Duration}; 29 | 30 | use crate::persistence::types::{Channel, Contest, DBKey, NameKey, User}; 31 | use crate::telegram::channels; 32 | use crate::telegram::commands::start; 33 | use crate::telegram::contests; 34 | use crate::telegram::messages::{ 35 | delete_message, display_main_commands, display_manage_menu, escape_markdown, 36 | remove_loading_icon, 37 | }; 38 | use crate::telegram::users; 39 | 40 | /// Callback function invoked every time Telegram sends a callback message. 41 | /// It implements the FSM for the contests management. It alwasy refers to a chan. 42 | /// In fact, the variable `chan_id` in the code is always defined. 43 | /// 44 | /// # Arguments 45 | /// * `ctx` - Telexide context 46 | /// * `update` - The update message received 47 | /// 48 | /// # Panics 49 | /// Panics if the connection to the DB fails or if telegram returns an error. 50 | #[prepare_listener] 51 | pub async fn callback(ctx: Context, update: Update) { 52 | let callback = match update.content { 53 | UpdateContent::CallbackQuery(ref q) => q, 54 | _ => return, 55 | }; 56 | let parent_message = callback.message.clone().unwrap().message_id; 57 | let chat_id = callback.message.clone().unwrap().chat.get_id(); 58 | let sender_id = callback.from.id; 59 | 60 | let data = callback.data.clone().unwrap_or_else(String::new); 61 | let mut source: i64 = 0; 62 | let mut dest: i64 = 0; 63 | let chan_id: i64; 64 | // Accepted invitation 65 | let mut accepted = false; 66 | let mut manage = false; 67 | // Manage commands 68 | let (mut create, mut delete, mut stop, mut start, mut list) = 69 | (false, false, false, false, false); 70 | // Back to main menu 71 | let mut main = false; 72 | // Start/Stop/Delete Contest commands 73 | let (mut start_contest, mut delete_contest, mut stop_contest) = (false, false, false); 74 | let mut contest_id = 0; 75 | if data.contains('✅') { 76 | let mut iter = data.split_ascii_whitespace(); 77 | iter.next(); // tick 78 | source = iter.next().unwrap().parse().unwrap(); // source user 79 | dest = iter.next().unwrap().parse().unwrap(); // dest user 80 | chan_id = iter.next().unwrap().parse().unwrap(); // channel id 81 | contest_id = iter.next().unwrap().parse().unwrap(); // contest id 82 | accepted = true; 83 | } else if data.contains('❌') { 84 | // Rejected invitation 85 | let text = Some("Ok, doing nothing.".to_string()); 86 | let res = ctx 87 | .api 88 | .answer_callback_query(AnswerCallbackQuery { 89 | callback_query_id: callback.id.clone(), 90 | cache_time: None, 91 | show_alert: false, 92 | text, 93 | url: None, 94 | }) 95 | .await; 96 | if res.is_err() { 97 | error!("[callback handler] {}", res.err().unwrap()); 98 | } 99 | return; 100 | } else if data.starts_with("manage") { 101 | let mut iter = data.split_ascii_whitespace(); 102 | iter.next(); // manage 103 | chan_id = iter.next().unwrap().parse().unwrap(); 104 | manage = true; 105 | } else if data.starts_with("main") { 106 | let mut iter = data.split_ascii_whitespace(); 107 | iter.next(); // main 108 | chan_id = iter.next().unwrap().parse().unwrap(); 109 | main = true; 110 | } else if data.starts_with("create") { 111 | let mut iter = data.split_ascii_whitespace(); 112 | iter.next(); // delete 113 | chan_id = iter.next().unwrap().parse().unwrap(); 114 | create = true; 115 | } else if data.starts_with("delete_contest") { 116 | let mut iter = data.split_ascii_whitespace(); 117 | iter.next(); // delete 118 | chan_id = iter.next().unwrap().parse().unwrap(); 119 | contest_id = iter.next().unwrap().parse().unwrap(); 120 | delete_contest = true; 121 | } else if data.starts_with("start_contest") { 122 | let mut iter = data.split_ascii_whitespace(); 123 | iter.next(); // start 124 | chan_id = iter.next().unwrap().parse().unwrap(); 125 | contest_id = iter.next().unwrap().parse().unwrap(); 126 | start_contest = true; 127 | } else if data.starts_with("stop_contest") { 128 | let mut iter = data.split_ascii_whitespace(); 129 | iter.next(); // start 130 | chan_id = iter.next().unwrap().parse().unwrap(); 131 | contest_id = iter.next().unwrap().parse().unwrap(); 132 | stop_contest = true; 133 | } else if data.starts_with("delete") { 134 | let mut iter = data.split_ascii_whitespace(); 135 | iter.next(); // delete 136 | chan_id = iter.next().unwrap().parse().unwrap(); 137 | delete = true; 138 | } else if data.starts_with("stop") { 139 | let mut iter = data.split_ascii_whitespace(); 140 | iter.next(); // stop 141 | chan_id = iter.next().unwrap().parse().unwrap(); 142 | stop = true; 143 | } else if data.starts_with("start") { 144 | let mut iter = data.split_ascii_whitespace(); 145 | iter.next(); // start 146 | chan_id = iter.next().unwrap().parse().unwrap(); 147 | start = true; 148 | } else if data.starts_with("list") { 149 | let mut iter = data.split_ascii_whitespace(); 150 | iter.next(); // start 151 | chan_id = iter.next().unwrap().parse().unwrap(); 152 | list = true; 153 | } else { 154 | // Anyway, on no-sense command reply with the empty message 155 | // to remove the loading icon next to the button 156 | let res = ctx 157 | .api 158 | .answer_callback_query(AnswerCallbackQuery { 159 | callback_query_id: callback.id.clone(), 160 | cache_time: None, 161 | show_alert: false, 162 | text: None, 163 | url: None, 164 | }) 165 | .await; 166 | if res.is_err() { 167 | error!("[callback handler] {}", res.err().unwrap()); 168 | } 169 | return; 170 | } 171 | 172 | // always check for the bot being administrato of the chan 173 | // identified by chat_id. 174 | // In case of errors, admins sends a message to the user 175 | // and this list is empty. 176 | let admins = channels::admins(&ctx, chan_id, sender_id).await; 177 | if admins.is_empty() { 178 | return; 179 | } 180 | 181 | if main { 182 | delete_message(&ctx, chat_id, parent_message).await; 183 | display_main_commands(&ctx, sender_id).await; 184 | return; 185 | } 186 | 187 | let chan = { 188 | let guard = ctx.data.read(); 189 | let map = guard.get::().expect("db"); 190 | let conn = map.get().unwrap(); 191 | 192 | let mut stmt = conn 193 | .prepare("SELECT link, name, registered_by FROM channels WHERE id = ?") 194 | .unwrap(); 195 | 196 | let channel = stmt 197 | .query_map(params![chan_id], |row| { 198 | Ok(Channel { 199 | id: chan_id, 200 | link: row.get(0)?, 201 | name: row.get(1)?, 202 | registered_by: row.get(2)?, 203 | }) 204 | }) 205 | .unwrap() 206 | .map(std::result::Result::unwrap) 207 | .next() 208 | .unwrap(); 209 | Some(channel) 210 | }; 211 | 212 | if chan.is_none() { 213 | return; 214 | } 215 | let chan = chan.unwrap(); 216 | 217 | if accepted { 218 | // getChatMember always returns a ChatMember, even if the user never joined the chan. 219 | // if the request fails, the user does not exists and we should exit 220 | // if the request is ok, we need to check the type of the ChatMember 221 | let member = ctx 222 | .api 223 | .get_chat_member(GetChatMember { 224 | chat_id: chan.id, 225 | user_id: sender_id, 226 | }) 227 | .await; 228 | 229 | let member_joined = |m: ChatMember| -> bool { 230 | match m { 231 | ChatMember::Administrator(_) 232 | | ChatMember::Creator(_) 233 | | ChatMember::Member(_) 234 | | ChatMember::Restricted(_) => true, 235 | ChatMember::Kicked(_) | ChatMember::Left(_) => false, 236 | } 237 | }; 238 | match member { 239 | Ok(m) => { 240 | if member_joined(m) { 241 | let text = format!( 242 | "You are already a member of [{}]({})\\.", 243 | escape_markdown(&chan.name.to_string(), None), 244 | chan.link 245 | ); 246 | let mut reply = SendMessage::new(sender_id, &text); 247 | reply.set_parse_mode(&ParseMode::MarkdownV2); 248 | let res = ctx.api.send_message(reply).await; 249 | if res.is_err() { 250 | let err = res.err().unwrap(); 251 | error!("[already member] {}", err); 252 | } 253 | remove_loading_icon(&ctx, &callback.id, None).await; 254 | return; 255 | } 256 | } 257 | Err(err) => { 258 | let text = escape_markdown(&format!("{err}"), None); 259 | let mut reply = SendMessage::new(sender_id, &text); 260 | reply.set_parse_mode(&ParseMode::MarkdownV2); 261 | let res = ctx.api.send_message(reply).await; 262 | if res.is_err() { 263 | let err = res.err().unwrap(); 264 | error!("[already member] {}", err); 265 | } 266 | remove_loading_icon(&ctx, &callback.id, None).await; 267 | return; 268 | } 269 | } 270 | 271 | let res = ctx 272 | .api 273 | .answer_callback_query(AnswerCallbackQuery { 274 | callback_query_id: callback.id.clone(), 275 | cache_time: None, 276 | show_alert: false, 277 | text: None, 278 | url: None, 279 | }) 280 | .await; 281 | if res.is_err() { 282 | error!("[callback handler] {}", res.err().unwrap()); 283 | } 284 | let text = format!( 285 | "Please join \u{1f449} [{}]({}) within the next 10 seconds\\.", 286 | escape_markdown(&chan.name.to_string(), None), 287 | chan.link 288 | ); 289 | let mut reply = SendMessage::new(sender_id, &text); 290 | reply.set_parse_mode(&ParseMode::MarkdownV2); 291 | let res = ctx.api.send_message(reply).await; 292 | if res.is_err() { 293 | let err = res.err().unwrap(); 294 | error!("[please join] {}", err); 295 | } 296 | 297 | sleep(Duration::from_secs(10)).await; 298 | let member = ctx 299 | .api 300 | .get_chat_member(GetChatMember { 301 | chat_id: chan.id, 302 | user_id: sender_id, 303 | }) 304 | .await; 305 | 306 | // The unwrap is likely to not fail, since the previous request is identical and succeded 307 | let joined = member_joined(member.unwrap()); 308 | if joined { 309 | info!("Refer OK!"); 310 | let c = contests::get(&ctx, contest_id); 311 | if c.is_none() { 312 | error!("[refer ok] Invalid contest passed in url"); 313 | let res = ctx 314 | .api 315 | .send_message(SendMessage::new( 316 | sender_id, 317 | "You joined the channel but the contest does not exist.", 318 | )) 319 | .await; 320 | if res.is_err() { 321 | let err = res.err().unwrap(); 322 | error!("[failed to insert invitation] {}", err); 323 | } 324 | } else { 325 | let c = c.unwrap(); 326 | let now: DateTime = Utc::now(); 327 | if now > c.end { 328 | info!("Joining with expired contest"); 329 | let res = ctx 330 | .api 331 | .send_message(SendMessage::new( 332 | sender_id, 333 | "You joined the group/channel but the contest is finished", 334 | )) 335 | .await; 336 | if res.is_err() { 337 | let err = res.err().unwrap(); 338 | error!("[failed to insert invitation] {}", err); 339 | } 340 | } else { 341 | let res = { 342 | let guard = ctx.data.read(); 343 | let map = guard.get::().expect("db"); 344 | let conn = map.get().unwrap(); 345 | conn.execute( 346 | "INSERT INTO invitations(source, dest, chan, contest) VALUES(?, ?, ?, ?)", 347 | params![source, dest, chan.id, contest_id], 348 | ) 349 | }; 350 | if res.is_err() { 351 | let err = res.err().unwrap(); 352 | error!("[insert invitation] {}", err); 353 | let res = ctx 354 | .api 355 | .send_message(SendMessage::new( 356 | sender_id, 357 | "Failed to insert invitation: this invitation might already exist!", 358 | )) 359 | .await; 360 | if res.is_err() { 361 | let err = res.err().unwrap(); 362 | error!("[failed to insert invitation] {}", err); 363 | } 364 | } else { 365 | let text = format!( 366 | "You joined [{}]({}) \u{1f917}", 367 | escape_markdown(&chan.name.to_string(), None), 368 | chan.link 369 | ); 370 | let mut reply = SendMessage::new(sender_id, &text); 371 | reply.set_parse_mode(&ParseMode::MarkdownV2); 372 | let res = ctx.api.send_message(reply).await; 373 | if res.is_err() { 374 | let err = res.err().unwrap(); 375 | error!("[joined send] {}", err); 376 | } 377 | } 378 | } 379 | } 380 | } else { 381 | info!("User not joined the channel after 10 seconds..."); 382 | let text = escape_markdown("You haven't joined the channel within 10 seconds :(", None); 383 | let mut reply = SendMessage::new(sender_id, &text); 384 | reply.set_parse_mode(&ParseMode::MarkdownV2); 385 | let res = ctx.api.send_message(reply).await; 386 | if res.is_err() { 387 | let err = res.err().unwrap(); 388 | error!("[not join] {}", err); 389 | } 390 | } 391 | delete_message(&ctx, chat_id, parent_message).await; 392 | } 393 | 394 | if manage { 395 | remove_loading_icon(&ctx, &callback.id, None).await; 396 | display_manage_menu(&ctx, chat_id, &chan).await; 397 | delete_message(&ctx, chat_id, parent_message).await; 398 | } 399 | 400 | if start { 401 | let contests = contests::get_all(&ctx, chan.id) 402 | .into_iter() 403 | .filter(|c| c.started_at.is_none()) 404 | .collect::>(); 405 | if contests.is_empty() { 406 | remove_loading_icon(&ctx, &callback.id, Some("You have no contests to start!")).await; 407 | } else { 408 | let mut reply = SendMessage::new( 409 | sender_id, 410 | &escape_markdown("Select the contest to start", None), 411 | ); 412 | let mut partition_size: usize = contests.len() / 2; 413 | if partition_size < 2 { 414 | partition_size = 1; 415 | } 416 | let inline_keyboard: Vec> = contests 417 | .chunks(partition_size) 418 | .map(|chunk| { 419 | chunk 420 | .iter() 421 | .map(|contest| InlineKeyboardButton { 422 | text: contest.name.clone(), 423 | // delete_contest, channel id, contest id 424 | callback_data: Some(format!( 425 | "start_contest {} {}", 426 | chan.id, contest.id 427 | )), 428 | callback_game: None, 429 | login_url: None, 430 | pay: None, 431 | switch_inline_query: None, 432 | switch_inline_query_current_chat: None, 433 | url: None, 434 | }) 435 | .collect() 436 | }) 437 | .collect(); 438 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 439 | inline_keyboard, 440 | })); 441 | reply.set_parse_mode(&ParseMode::MarkdownV2); 442 | 443 | let res = ctx.api.send_message(reply).await; 444 | if res.is_err() { 445 | let err = res.err().unwrap(); 446 | error!("[start send] {}", err); 447 | } 448 | remove_loading_icon(&ctx, &callback.id, None).await; 449 | delete_message(&ctx, chat_id, parent_message).await; 450 | }; 451 | } 452 | 453 | if stop { 454 | let contests = contests::get_all(&ctx, chan.id) 455 | .into_iter() 456 | .filter(|c| c.started_at.is_some() && !c.stopped) 457 | .collect::>(); 458 | if contests.is_empty() { 459 | remove_loading_icon(&ctx, &callback.id, Some("You have no contests to stop!")).await; 460 | } else { 461 | let mut reply = SendMessage::new( 462 | chat_id, 463 | &escape_markdown("Select the contest to stop", None), 464 | ); 465 | let mut partition_size: usize = contests.len() / 2; 466 | if partition_size < 2 { 467 | partition_size = 1; 468 | } 469 | let inline_keyboard: Vec> = contests 470 | .chunks(partition_size) 471 | .map(|chunk| { 472 | chunk 473 | .iter() 474 | .map(|contest| InlineKeyboardButton { 475 | text: contest.name.clone(), 476 | // stop_contest, channel id, contest id 477 | callback_data: Some(format!("stop_contest {} {}", chan.id, contest.id)), 478 | callback_game: None, 479 | login_url: None, 480 | pay: None, 481 | switch_inline_query: None, 482 | switch_inline_query_current_chat: None, 483 | url: None, 484 | }) 485 | .collect() 486 | }) 487 | .collect(); 488 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 489 | inline_keyboard, 490 | })); 491 | reply.set_parse_mode(&ParseMode::MarkdownV2); 492 | 493 | let res = ctx.api.send_message(reply).await; 494 | if res.is_err() { 495 | let err = res.err().unwrap(); 496 | error!("[create send] {}", err); 497 | } 498 | remove_loading_icon(&ctx, &callback.id, None).await; 499 | delete_message(&ctx, chat_id, parent_message).await; 500 | }; 501 | } 502 | 503 | if stop_contest { 504 | // Clean up ranks from users that joined and then left the channel 505 | let c = contests::get(&ctx, contest_id).unwrap(); 506 | if c.stopped { 507 | let reply = SendMessage::new(chat_id, "Contest already stopped. Doing nothing."); 508 | let res = ctx.api.send_message(reply).await; 509 | if res.is_err() { 510 | let err = res.err().unwrap(); 511 | error!("[stop send] {}", err); 512 | } 513 | display_manage_menu(&ctx, chat_id, &chan).await; 514 | delete_message(&ctx, chat_id, parent_message).await; 515 | } else { 516 | contests::validate_users(&ctx, &c).await; 517 | 518 | // Stop contest on db 519 | let c = { 520 | let guard = ctx.data.read(); 521 | let map = guard.get::().expect("db"); 522 | let conn = map.get().unwrap(); 523 | let mut stmt = conn.prepare("UPDATE contests SET stopped = TRUE WHERE id = ? RETURNING name, prize, end, started_at").unwrap(); 524 | let mut iter = stmt 525 | .query_map(params![contest_id], |row| { 526 | Ok(Contest { 527 | id: contest_id, 528 | name: row.get(0)?, 529 | prize: row.get(1)?, 530 | end: row.get(2)?, 531 | started_at: row.get(3)?, 532 | stopped: true, 533 | chan: chan.id, 534 | }) 535 | }) 536 | .unwrap(); 537 | iter.next().unwrap().unwrap() 538 | }; 539 | 540 | // Create rank 541 | let rank = contests::ranking(&ctx, &c); 542 | if rank.is_empty() { 543 | // No one partecipated in the challenge 544 | let reply = SendMessage::new( 545 | sender_id, 546 | "No one partecipated to the challenge. Doing nothing.", 547 | ); 548 | let res = ctx.api.send_message(reply).await; 549 | if res.is_err() { 550 | let err = res.err().unwrap(); 551 | error!("[stop send] {}", err); 552 | } 553 | display_manage_menu(&ctx, chat_id, &chan).await; 554 | delete_message(&ctx, chat_id, parent_message).await; 555 | } else { 556 | // Send top-10 to the channel and pin the message 557 | let mut m = format!("\u{1f3c6} Contest ({}) finished \u{1f3c6}\n\n\n", c.name); 558 | let winner = rank[0].user.clone(); 559 | for row in rank { 560 | let user = row.user; 561 | let rank = row.rank; 562 | let invites = row.invites; 563 | if rank == 1 { 564 | m += "\u{1f947}#1!"; 565 | } else if rank <= 3 { 566 | m += &format!("\u{1f3c6} #{rank}"); 567 | } else { 568 | m += &format!("#{rank}"); 569 | } 570 | 571 | m += &format!( 572 | " {}{}{} - {}\n", 573 | user.first_name, 574 | match user.last_name { 575 | Some(last_name) => format!(" {last_name}"), 576 | None => String::new(), 577 | }, 578 | match user.username { 579 | Some(username) => format!(" ({username})"), 580 | None => String::new(), 581 | }, 582 | invites 583 | ); 584 | } 585 | m += &format!( 586 | "\n\nThe prize ({}) is being delivered to our champion \u{1f947}. Congratulations!!", 587 | c.prize 588 | ); 589 | 590 | m = escape_markdown(&m, None); 591 | 592 | let mut reply = SendMessage::new(c.chan, &m); 593 | reply.set_parse_mode(&ParseMode::MarkdownV2); 594 | let res = ctx.api.send_message(reply).await; 595 | if res.is_err() { 596 | let err = res.unwrap_err(); 597 | error!("[send message] {}", err); 598 | } else { 599 | // Pin message 600 | let res = ctx 601 | .api 602 | .pin_chat_message(PinChatMessage { 603 | chat_id: c.chan, 604 | message_id: res.unwrap().message_id, 605 | disable_notification: false, 606 | }) 607 | .await; 608 | if res.is_err() { 609 | let err = res.unwrap_err(); 610 | error!("[stop pin message] {}", err); 611 | let reply = SendMessage::new(sender_id, &err.to_string()); 612 | let res = ctx.api.send_message(reply).await; 613 | if res.is_err() { 614 | error!("[stop pin message2] {}", res.unwrap_err()); 615 | } 616 | } 617 | } 618 | 619 | // Put into communication the bot user and the winner 620 | let direct_communication = winner.username.is_some(); 621 | let text = if direct_communication { 622 | let username = winner.username.unwrap(); 623 | format!("The winner usename is @{username}. Get in touch and send the prize!") 624 | } else { 625 | "The winner has no username. It means you can communicate only through the bot.\n\n\ 626 | Write NOW a message that will be delivered to the winner (if you can, just send the prize!).\n\n 627 | NOTE: You can only send up to one message, hence a good idea is to share your username with the winner\ 628 | in order to make they start a commucation with you in private.".to_string() 629 | }; 630 | let mut reply = SendMessage::new(sender_id, &escape_markdown(&text, None)); 631 | reply.set_parse_mode(&ParseMode::MarkdownV2); 632 | let res = ctx.api.send_message(reply).await; 633 | if res.is_err() { 634 | let err = res.err().unwrap(); 635 | error!("[stop send] {}", err); 636 | } 637 | if !direct_communication { 638 | // Outside of FSM 639 | let res = { 640 | let guard = ctx.data.read(); 641 | let map = guard.get::().expect("db"); 642 | let conn = map.get().unwrap(); 643 | // add user to contact, the owner (me), the contest 644 | // in order to add more constraint to verify outside of this FMS 645 | // to validate and put the correct owner in contact with the correct winner 646 | conn.execute( 647 | "INSERT INTO being_contacted_users(user, owner) VALUES(?, ?)", 648 | params![winner.id, sender_id], 649 | ) 650 | }; 651 | 652 | if res.is_err() { 653 | let err = res.err().unwrap(); 654 | error!("[insert being_contacted_users] {}", err); 655 | } 656 | } 657 | } 658 | } 659 | 660 | remove_loading_icon(&ctx, &callback.id, None).await; 661 | } 662 | 663 | if create { 664 | let now: DateTime = Utc::now(); 665 | let mut reply = SendMessage::new( 666 | sender_id, 667 | &escape_markdown( 668 | &format!( 669 | "Write a single message with every required info on a new line\n\n\ 670 | Contest name\n\ 671 | End date (YYYY-MM-DD hh:mm TZ)\n\ 672 | Prize\n\n\ 673 | For example a valid message is (note the GMT+1 timezone written as +01):\n\n\ 674 | {month_string} {year}\n\ 675 | {year}-{month}-28 20:00 +01\n\ 676 | Amazon 50\u{20ac} Gift Card\n", 677 | year = now.format("%Y"), 678 | month = now.format("%m"), 679 | month_string = now.format("%B") 680 | ), 681 | None, 682 | ), 683 | ); 684 | reply.set_parse_mode(&ParseMode::MarkdownV2); 685 | 686 | let res = ctx.api.send_message(reply).await; 687 | if res.is_err() { 688 | let err = res.err().unwrap(); 689 | error!("[create send] {}", err); 690 | } 691 | 692 | // adding chan to being_managed_channels since the raw 693 | // reply falls outiside this FSM 694 | let res = { 695 | let guard = ctx.data.read(); 696 | let map = guard.get::().expect("db"); 697 | let conn = map.get().unwrap(); 698 | conn.execute( 699 | "INSERT INTO being_managed_channels(chan) VALUES(?)", 700 | params![chan.id], 701 | ) 702 | }; 703 | 704 | if res.is_err() { 705 | let err = res.err().unwrap(); 706 | error!("[insert being_managed_channels] {}", err); 707 | } 708 | 709 | remove_loading_icon(&ctx, &callback.id, None).await; 710 | delete_message(&ctx, chat_id, parent_message).await; 711 | } 712 | 713 | if delete { 714 | let contests = contests::get_all(&ctx, chan.id); 715 | if contests.is_empty() { 716 | remove_loading_icon(&ctx, &callback.id, Some("You have no contests to delete!")).await; 717 | } else { 718 | let mut reply = SendMessage::new( 719 | sender_id, 720 | &escape_markdown("Select the contest to delete", None), 721 | ); 722 | let mut partition_size: usize = contests.len() / 2; 723 | if partition_size < 2 { 724 | partition_size = 1; 725 | } 726 | let inline_keyboard: Vec> = contests 727 | .chunks(partition_size) 728 | .map(|chunk| { 729 | chunk 730 | .iter() 731 | .map(|contest| InlineKeyboardButton { 732 | text: contest.name.clone(), 733 | // delete_contest, channel id, contest id 734 | callback_data: Some(format!( 735 | "delete_contest {} {}", 736 | chan.id, contest.id 737 | )), 738 | callback_game: None, 739 | login_url: None, 740 | pay: None, 741 | switch_inline_query: None, 742 | switch_inline_query_current_chat: None, 743 | url: None, 744 | }) 745 | .collect() 746 | }) 747 | .collect(); 748 | reply.set_reply_markup(&ReplyMarkup::InlineKeyboardMarkup(InlineKeyboardMarkup { 749 | inline_keyboard, 750 | })); 751 | reply.set_parse_mode(&ParseMode::MarkdownV2); 752 | 753 | let res = ctx.api.send_message(reply).await; 754 | if res.is_err() { 755 | let err = res.err().unwrap(); 756 | error!("[create send] {}", err); 757 | } 758 | remove_loading_icon(&ctx, &callback.id, None).await; 759 | delete_message(&ctx, chat_id, parent_message).await; 760 | }; 761 | } 762 | 763 | if list { 764 | let text = { 765 | let contests = contests::get_all(&ctx, chan.id); 766 | let mut text: String = String::new(); 767 | if !contests.is_empty() { 768 | text += "```\n"; 769 | let mut table = Table::new("{:<} | {:<} | {:<} | {:<} | {:<} | {:<}"); 770 | table.add_row( 771 | Row::new() 772 | .with_cell("Name") 773 | .with_cell("End") 774 | .with_cell("Prize") 775 | .with_cell("Started") 776 | .with_cell("Stopped") 777 | .with_cell("Users"), 778 | ); 779 | for (_, contest) in contests.iter().enumerate() { 780 | let users = contests::count_users(&ctx, contest); 781 | table.add_row( 782 | Row::new() 783 | .with_cell(&contest.name) 784 | .with_cell(contest.end) 785 | .with_cell(&contest.prize) 786 | .with_cell(match contest.started_at { 787 | Some(x) => format!("{x}"), 788 | None => "No".to_string(), 789 | }) 790 | .with_cell(if contest.stopped { 791 | "Yes".to_string() 792 | } else { 793 | "No".to_string() 794 | }) 795 | .with_cell(users), 796 | ); 797 | } 798 | text += &format!( 799 | "{}```\n\n{}", 800 | table, 801 | escape_markdown( 802 | "Dates are all converted to UTC timezone.\nBetter view on desktop.", 803 | None 804 | ) 805 | ); 806 | } 807 | text 808 | }; 809 | 810 | if text.is_empty() { 811 | remove_loading_icon( 812 | &ctx, 813 | &callback.id, 814 | Some("You don't have any active or past contests for this group/channel!"), 815 | ) 816 | .await; 817 | } else { 818 | let mut reply = SendMessage::new(sender_id, &text); 819 | reply.set_parse_mode(&ParseMode::MarkdownV2); 820 | 821 | let res = ctx.api.send_message(reply).await; 822 | 823 | if res.is_err() { 824 | let err = res.err().unwrap(); 825 | error!("[list contests] {}", err); 826 | } 827 | remove_loading_icon(&ctx, &callback.id, None).await; 828 | 829 | display_manage_menu(&ctx, chat_id, &chan).await; 830 | delete_message(&ctx, chat_id, parent_message).await; 831 | } 832 | } 833 | 834 | if delete_contest { 835 | let res = { 836 | let guard = ctx.data.read(); 837 | let map = guard.get::().expect("db"); 838 | let conn = map.get().unwrap(); 839 | let mut stmt = conn.prepare("DELETE FROM contests WHERE id = ?").unwrap(); 840 | stmt.execute(params![contest_id]) 841 | }; 842 | let text = if res.is_err() { 843 | let err = res.unwrap_err(); 844 | error!("[delete from contests] {}", err); 845 | format!("Error: {err}. You can't stop a contest with already some partecipant, this is unfair!") 846 | } else { 847 | "Done!".to_string() 848 | }; 849 | let res = ctx 850 | .api 851 | .send_message(SendMessage::new(sender_id, &text)) 852 | .await; 853 | if res.is_err() { 854 | let err = res.err().unwrap(); 855 | error!("[send message delete contest] {}", err); 856 | } 857 | 858 | remove_loading_icon(&ctx, &callback.id, None).await; 859 | display_manage_menu(&ctx, chat_id, &chan).await; 860 | delete_message(&ctx, chat_id, parent_message).await; 861 | } 862 | 863 | if start_contest { 864 | // if contest_id is not valid, this panics (that's ok, the user is doing nasty things) 865 | let c = contests::get(&ctx, contest_id).unwrap(); 866 | if c.started_at.is_some() { 867 | let text = "You can't start an already started contest."; 868 | let res = ctx 869 | .api 870 | .send_message(SendMessage::new(sender_id, text)) 871 | .await; 872 | if res.is_err() { 873 | let err = res.err().unwrap(); 874 | error!("[send message] {}", err); 875 | } 876 | } else { 877 | let c = { 878 | let now: DateTime = Utc::now(); 879 | let guard = ctx.data.read(); 880 | let map = guard.get::().expect("db"); 881 | let conn = map.get().unwrap(); 882 | let mut stmt = conn.prepare("UPDATE contests SET started_at = ? WHERE id = ? RETURNING name, prize, end").unwrap(); 883 | let mut iter = stmt 884 | .query_map(params![now, contest_id], |row| { 885 | Ok(Contest { 886 | id: contest_id, 887 | name: row.get(0)?, 888 | prize: row.get(1)?, 889 | end: row.get(2)?, 890 | started_at: Some(now), 891 | stopped: false, 892 | chan: chan.id, 893 | }) 894 | }) 895 | .unwrap(); 896 | iter.next().unwrap() 897 | }; 898 | let text = if c.is_err() { 899 | let err = c.as_ref().err().unwrap(); 900 | error!("[update/start contest] {}", err); 901 | err.to_string() 902 | } else { 903 | "Contest started!".to_string() 904 | }; 905 | let res = ctx 906 | .api 907 | .send_message(SendMessage::new(sender_id, &text)) 908 | .await; 909 | if res.is_err() { 910 | let err = res.err().unwrap(); 911 | error!("[send message] {}", err); 912 | } 913 | 914 | if c.is_ok() { 915 | let c = c.unwrap(); 916 | // Send message in the channel, indicating the contest name 917 | // the end date, the prize, and pin it on top until the end date comes 918 | // or the contest is stopped or deleted 919 | let bot_name = { 920 | let guard = ctx.data.read(); 921 | guard 922 | .get::() 923 | .expect("name") 924 | .clone() 925 | .replace('@', "") 926 | }; 927 | let params = 928 | BASE64URL.encode(format!("chan={}&contest={}", chan.id, c.id).as_bytes()); 929 | let text = format!( 930 | "{title}\n\n{rules}\n\n{bot_link}", 931 | title = escape_markdown( 932 | &format!( 933 | "\u{1f525}{name} contest \u{1f525}\nWho invites more friends wins a {prize}!", 934 | prize = c.prize, 935 | name = c.name 936 | ), 937 | None 938 | ), 939 | rules = format!( 940 | "{} **{prize}**\n{disclaimer}", 941 | escape_markdown( 942 | &format!( 943 | "1. Start the contest bot using the link below\n\ 944 | 2. The bot gives you a link\n\ 945 | 3. Share the link with your friends!\n\n\ 946 | At the end of the contest ({end_date}) the user that referred more friends \ 947 | will win a ", 948 | end_date = c.end 949 | ), 950 | None 951 | ), 952 | prize = escape_markdown(&c.prize, None), 953 | disclaimer = 954 | escape_markdown("You can check your rank with the /rank command", None), 955 | ), 956 | bot_link = escape_markdown( 957 | &format!( 958 | "https://t.me/{bot_name}?start={params}" 959 | ), 960 | None 961 | ), 962 | ); 963 | 964 | let mut reply = SendMessage::new(c.chan, &text); 965 | reply.set_parse_mode(&ParseMode::MarkdownV2); 966 | let res = ctx.api.send_message(reply).await; 967 | if res.is_err() { 968 | let err = res.unwrap_err(); 969 | error!("[send message] {}", err); 970 | } else { 971 | // Pin message 972 | let res = ctx 973 | .api 974 | .pin_chat_message(PinChatMessage { 975 | chat_id: c.chan, 976 | message_id: res.unwrap().message_id, 977 | disable_notification: false, 978 | }) 979 | .await; 980 | if res.is_err() { 981 | let err = res.unwrap_err(); 982 | error!("[pin message] {}", err); 983 | let reply = SendMessage::new(sender_id, &err.to_string()); 984 | let res = ctx.api.send_message(reply).await; 985 | if res.is_err() { 986 | error!("[pin message2] {}", res.unwrap_err()); 987 | } 988 | } 989 | } 990 | } 991 | } 992 | 993 | remove_loading_icon(&ctx, &callback.id, None).await; 994 | display_manage_menu(&ctx, chat_id, &chan).await; 995 | delete_message(&ctx, chat_id, parent_message).await; 996 | } 997 | } 998 | 999 | /// Callback function invoked every time an user send a text message to RaF. 1000 | /// It handles the behavior of the BoT in groups or when an owners is creating a context. 1001 | /// Also manages the final stage, when the owner is contacting a winner that has no username, 1002 | /// trough RaF. 1003 | /// 1004 | /// # Arguments 1005 | /// * `ctx` - Telexide context 1006 | /// * `update` - The update message received 1007 | /// 1008 | /// # Panics 1009 | /// Panics if the connection to the DB fails or if telegram returns an error. 1010 | #[prepare_listener] 1011 | pub async fn message(ctx: Context, update: Update) { 1012 | info!("message handler begin"); 1013 | let message = match update.content { 1014 | UpdateContent::Message(ref m) => m, 1015 | _ => return, 1016 | }; 1017 | let sender_id = message.from.clone().unwrap().id; 1018 | 1019 | // If the user if forwarding a message from a channel, we are in the registration flow. 1020 | // NOTE: we can extract info from the source chat, only in case of channels. 1021 | // For (super)groups we need to have the bot inside the (super)group and receive 1022 | // a message. 1023 | let chat_id: Option = { 1024 | message.forward_data.as_ref().and_then(|forward_data| { 1025 | forward_data 1026 | .from_chat 1027 | .as_ref() 1028 | .and_then(|from_chat| match from_chat { 1029 | Chat::Channel(c) => Some(c.id), 1030 | _ => None, 1031 | }) 1032 | }) 1033 | }; 1034 | 1035 | let registration_flow = chat_id.is_some(); 1036 | if registration_flow { 1037 | let chat_id = chat_id.unwrap(); 1038 | let registered_by = message.from.clone().unwrap().id; 1039 | channels::try_register(&ctx, chat_id, registered_by).await; 1040 | display_main_commands(&ctx, sender_id).await; 1041 | } else { 1042 | // If we are not in the channel registration flow, we just received a message 1043 | // and we should check if the message is among the accepted ones. 1044 | // 1045 | // It can be a group registration flow, or a channel begin managed, or other. 1046 | let text = message.get_text(); 1047 | if text.is_none() { 1048 | return; 1049 | } 1050 | let text = text.unwrap(); 1051 | // We also receive commands in this handler, we need to skip them or correctly forward them 1052 | // in case of commands that are used inside groups, e.g. 1053 | // /start@bot_name 1054 | if text.starts_with('/') { 1055 | let owners = users::owners(&ctx) 1056 | .iter() 1057 | .map(|u| u.id) 1058 | .collect::>(); 1059 | let is_owner = owners.iter().any(|&id| id == sender_id); 1060 | let bot_name = { 1061 | let guard = ctx.data.read(); 1062 | guard 1063 | .get::() 1064 | .expect("name") 1065 | .clone() 1066 | .replace('@', "") 1067 | }; 1068 | if text.starts_with(&format!("/start@{bot_name}")) && is_owner { 1069 | let res = start(ctx, message.clone()).await; 1070 | if res.is_err() { 1071 | error!("[inner start] {:?}", res.unwrap_err()); 1072 | } 1073 | } else { 1074 | let commands = vec!["help", "register", "contest", "list", "rank"]; 1075 | for command in commands { 1076 | if text.starts_with(&format!("/{command}@{bot_name}")) { 1077 | let chat_id = message.chat.get_id(); 1078 | let text = format!("All the commands, except for /start are disabled in groups. /start is enabled only for the group owner.\n\nTo use them, start @{bot_name}"); 1079 | let res = ctx.api.send_message(SendMessage::new(chat_id, &text)).await; 1080 | 1081 | if res.is_err() { 1082 | let err = res.err().unwrap(); 1083 | error!("[disabled commands in groups] {}", err); 1084 | } 1085 | break; 1086 | } 1087 | } 1088 | } 1089 | return; 1090 | } 1091 | 1092 | // From here below, we are interested only in messages sent from owners 1093 | let owners = users::owners(&ctx) 1094 | .iter() 1095 | .map(|u| u.id) 1096 | .collect::>(); 1097 | let is_owner = owners.iter().any(|&id| id == sender_id); 1098 | if !is_owner { 1099 | return; 1100 | } 1101 | 1102 | // Check if some of the user channel's are being managed 1103 | // in that case it's plausible that the user is sending the message in this format 1104 | // ``` 1105 | // contest name 1106 | // end date (YYYY-MM-DD hh:mm TZ) 1107 | // prize 1108 | // ``` 1109 | if text.split('\n').skip_while(|r| r.is_empty()).count() == 3 { 1110 | let channels = channels::get_all(&ctx, sender_id); // channels registered by the user 1111 | let chan = { 1112 | let guard = ctx.data.read(); 1113 | let map = guard.get::().expect("db"); 1114 | let conn = map.get().unwrap(); 1115 | // In the begin_managed_channels we have all the channels ever managed, we can order 1116 | // them by ID and keep only tha latest one, since there can be only one managed channel 1117 | // at a time, by the same user. 1118 | let mut stmt = conn 1119 | .prepare(&format!( 1120 | "SELECT channels.id, channels.link, channels.name, channels.registered_by FROM \ 1121 | channels INNER JOIN being_managed_channels ON channels.id = being_managed_channels.chan \ 1122 | WHERE being_managed_channels.chan IN ({}) ORDER BY being_managed_channels.id DESC LIMIT 1", 1123 | channels 1124 | .iter() 1125 | .map(|c| c.id.to_string()) 1126 | .collect::>() 1127 | .join(",") 1128 | )) 1129 | .unwrap(); 1130 | let chan = stmt 1131 | .query_map(params![], |row| { 1132 | Ok(Channel { 1133 | id: row.get(0)?, 1134 | link: row.get(1)?, 1135 | name: row.get(2)?, 1136 | registered_by: row.get(3)?, 1137 | }) 1138 | }) 1139 | .unwrap() 1140 | .map(Result::unwrap) 1141 | .next(); 1142 | chan 1143 | }; 1144 | if chan.is_some() { 1145 | let chan = chan.unwrap(); 1146 | let contest = contests::from_text(&text, chan.id); 1147 | 1148 | if let Ok(contest) = contest { 1149 | let res = { 1150 | let guard = ctx.data.read(); 1151 | let map = guard.get::().expect("db"); 1152 | let conn = map.get().unwrap(); 1153 | conn.execute( 1154 | "INSERT INTO contests(name, end, prize, chan) VALUES(?, ?, ?, ?)", 1155 | params![contest.name, contest.end, contest.prize, contest.chan], 1156 | ) 1157 | }; 1158 | 1159 | let text = if res.is_err() { 1160 | let err = res.err().unwrap(); 1161 | error!("[insert contest] {}", err); 1162 | format!("Error: {err}") 1163 | } else { 1164 | format!("Contest {} created succesfully!", contest.name) 1165 | }; 1166 | let res = ctx 1167 | .api 1168 | .send_message(SendMessage::new(sender_id, &text)) 1169 | .await; 1170 | 1171 | if res.is_err() { 1172 | let err = res.err().unwrap(); 1173 | error!("[contest ok send] {}", err); 1174 | } 1175 | } else { 1176 | let err = contest.unwrap_err(); 1177 | let res = ctx 1178 | .api 1179 | .send_message(SendMessage::new( 1180 | sender_id, 1181 | &format!( 1182 | "Something wrong happened while creating your new contest.\n\n\ 1183 | Error: {err}\n\n\ 1184 | Please restart the contest creating process and send a correct message" 1185 | ), 1186 | )) 1187 | .await; 1188 | 1189 | if res.is_err() { 1190 | let err = res.err().unwrap(); 1191 | error!("[contest ok send] {}", err); 1192 | } 1193 | } 1194 | // No need to delete the currently beign managed channel. We alwasy look for the last 1195 | // "being managed" inserted by this user 1196 | // NOTE: use sender_id instead of chat_id because this must go on the private chat 1197 | // user<->bot and not in the public chat. 1198 | display_manage_menu(&ctx, sender_id, &chan).await; 1199 | 1200 | // else, if no channel is being edited, but we received a 3 lines message 1201 | // it's just a message, do nothing (?) 1202 | } 1203 | } else { 1204 | // text splitted in a number of rows != 3 -> it can be a message 1205 | // being sent from an owner to a winner 1206 | let winner = { 1207 | let guard = ctx.data.read(); 1208 | let map = guard.get::().expect("db"); 1209 | let conn = map.get().unwrap(); 1210 | // In the being_contacted_users we have all the winner to be ever contacted 1211 | // we can join the contest and the owner and filter with the current user_id 1212 | // limiting only by the last one that matches all these conditions, to be almost 1213 | // sure to link the owner with the winner (correct pair) 1214 | let mut stmt = conn 1215 | .prepare( 1216 | "SELECT users.id, users.first_name, users.last_name, users.username FROM users \ 1217 | INNER JOIN being_contacted_users ON users.id = being_contacted_users.user \ 1218 | WHERE being_contacted_users.owner = ? AND being_contacted_users.contacted IS FALSE \ 1219 | ORDER BY being_contacted_users.id DESC LIMIT 1" 1220 | ) 1221 | .unwrap(); 1222 | let user = stmt 1223 | .query_map(params![sender_id], |row| { 1224 | Ok(User { 1225 | id: row.get(0)?, 1226 | first_name: row.get(1)?, 1227 | last_name: row.get(2)?, 1228 | username: row.get(3)?, 1229 | }) 1230 | }) 1231 | .unwrap() 1232 | .map(Result::unwrap) 1233 | .next(); 1234 | user 1235 | }; 1236 | if winner.is_some() { 1237 | let winner = winner.unwrap(); 1238 | let mut reply = SendMessage::new(winner.id, &text); 1239 | reply.set_parse_mode(&ParseMode::MarkdownV2); 1240 | let res = ctx.api.send_message(reply).await; 1241 | if res.is_err() { 1242 | let err = res.err().unwrap(); 1243 | error!("[winner communication] {}", err); 1244 | } else { 1245 | let reply = SendMessage::new(sender_id, "Message delivered to the winner!"); 1246 | let res = ctx.api.send_message(reply).await; 1247 | if res.is_err() { 1248 | let err = res.err().unwrap(); 1249 | error!("[winner postcom] {}", err); 1250 | } 1251 | // Set the winner user as contacted 1252 | let res = { 1253 | let guard = ctx.data.read(); 1254 | let map = guard.get::().expect("db"); 1255 | let conn = map.get().unwrap(); 1256 | // add user to contact, the owner (me), the contest 1257 | // in order to add more constraint to verify outside of this FMS 1258 | // to validate and put the correct owner in contact with the correct winner 1259 | conn.execute( 1260 | "UPDATE being_contacted_users SET contacted = TRUE WHERE owner = ? AND user = ?", 1261 | params![sender_id, winner.id], 1262 | ) 1263 | }; 1264 | 1265 | if res.is_err() { 1266 | let err = res.err().unwrap(); 1267 | error!("[insert being_contacted_users] {}", err); 1268 | } 1269 | } 1270 | 1271 | display_main_commands(&ctx, sender_id).await; 1272 | } 1273 | } 1274 | } 1275 | 1276 | info!("message handler end"); 1277 | } 1278 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android_system_properties" 16 | version = "0.1.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 | dependencies = [ 20 | "libc", 21 | ] 22 | 23 | [[package]] 24 | name = "async-stream" 25 | version = "0.3.6" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 28 | dependencies = [ 29 | "async-stream-impl", 30 | "futures-core", 31 | "pin-project-lite", 32 | ] 33 | 34 | [[package]] 35 | name = "async-stream-impl" 36 | version = "0.3.6" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 39 | dependencies = [ 40 | "proc-macro2", 41 | "quote", 42 | "syn 2.0.111", 43 | ] 44 | 45 | [[package]] 46 | name = "async-trait" 47 | version = "0.1.89" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 50 | dependencies = [ 51 | "proc-macro2", 52 | "quote", 53 | "syn 2.0.111", 54 | ] 55 | 56 | [[package]] 57 | name = "autocfg" 58 | version = "1.5.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 61 | 62 | [[package]] 63 | name = "axum" 64 | version = "0.6.20" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" 67 | dependencies = [ 68 | "async-trait", 69 | "axum-core", 70 | "bitflags 1.3.2", 71 | "bytes", 72 | "futures-util", 73 | "http 0.2.12", 74 | "http-body 0.4.6", 75 | "hyper 0.14.32", 76 | "itoa", 77 | "matchit", 78 | "memchr", 79 | "mime", 80 | "percent-encoding", 81 | "pin-project-lite", 82 | "rustversion", 83 | "serde", 84 | "sync_wrapper", 85 | "tower", 86 | "tower-layer", 87 | "tower-service", 88 | ] 89 | 90 | [[package]] 91 | name = "axum-core" 92 | version = "0.3.4" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" 95 | dependencies = [ 96 | "async-trait", 97 | "bytes", 98 | "futures-util", 99 | "http 0.2.12", 100 | "http-body 0.4.6", 101 | "mime", 102 | "rustversion", 103 | "tower-layer", 104 | "tower-service", 105 | ] 106 | 107 | [[package]] 108 | name = "base64" 109 | version = "0.21.7" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "1.3.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 118 | 119 | [[package]] 120 | name = "bitflags" 121 | version = "2.10.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 124 | 125 | [[package]] 126 | name = "bumpalo" 127 | version = "3.19.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 130 | 131 | [[package]] 132 | name = "bytes" 133 | version = "1.11.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 136 | 137 | [[package]] 138 | name = "cc" 139 | version = "1.2.48" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" 142 | dependencies = [ 143 | "find-msvc-tools", 144 | "shlex", 145 | ] 146 | 147 | [[package]] 148 | name = "cfg-if" 149 | version = "1.0.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 152 | 153 | [[package]] 154 | name = "chrono" 155 | version = "0.4.42" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 158 | dependencies = [ 159 | "iana-time-zone", 160 | "js-sys", 161 | "num-traits", 162 | "wasm-bindgen", 163 | "windows-link", 164 | ] 165 | 166 | [[package]] 167 | name = "colored" 168 | version = "3.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 171 | dependencies = [ 172 | "windows-sys 0.59.0", 173 | ] 174 | 175 | [[package]] 176 | name = "core-foundation" 177 | version = "0.9.4" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 180 | dependencies = [ 181 | "core-foundation-sys", 182 | "libc", 183 | ] 184 | 185 | [[package]] 186 | name = "core-foundation-sys" 187 | version = "0.8.7" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 190 | 191 | [[package]] 192 | name = "data-encoding" 193 | version = "2.9.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 196 | 197 | [[package]] 198 | name = "default" 199 | version = "0.1.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f33853de3c94db27f825215641be74ddce259f5aec7aefae9c760ee1bb3dbaa7" 202 | 203 | [[package]] 204 | name = "deranged" 205 | version = "0.5.5" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 208 | dependencies = [ 209 | "powerfmt", 210 | ] 211 | 212 | [[package]] 213 | name = "displaydoc" 214 | version = "0.2.5" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 217 | dependencies = [ 218 | "proc-macro2", 219 | "quote", 220 | "syn 2.0.111", 221 | ] 222 | 223 | [[package]] 224 | name = "equivalent" 225 | version = "1.0.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 228 | 229 | [[package]] 230 | name = "errno" 231 | version = "0.3.14" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 234 | dependencies = [ 235 | "libc", 236 | "windows-sys 0.61.2", 237 | ] 238 | 239 | [[package]] 240 | name = "fallible-iterator" 241 | version = "0.3.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 244 | 245 | [[package]] 246 | name = "fallible-streaming-iterator" 247 | version = "0.1.9" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 250 | 251 | [[package]] 252 | name = "fastrand" 253 | version = "2.3.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 256 | 257 | [[package]] 258 | name = "find-msvc-tools" 259 | version = "0.1.5" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 262 | 263 | [[package]] 264 | name = "fnv" 265 | version = "1.0.7" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 268 | 269 | [[package]] 270 | name = "foldhash" 271 | version = "0.1.5" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 274 | 275 | [[package]] 276 | name = "foreign-types" 277 | version = "0.3.2" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 280 | dependencies = [ 281 | "foreign-types-shared", 282 | ] 283 | 284 | [[package]] 285 | name = "foreign-types-shared" 286 | version = "0.1.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 289 | 290 | [[package]] 291 | name = "form_urlencoded" 292 | version = "1.2.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 295 | dependencies = [ 296 | "percent-encoding", 297 | ] 298 | 299 | [[package]] 300 | name = "futures" 301 | version = "0.3.31" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 304 | dependencies = [ 305 | "futures-channel", 306 | "futures-core", 307 | "futures-executor", 308 | "futures-io", 309 | "futures-sink", 310 | "futures-task", 311 | "futures-util", 312 | ] 313 | 314 | [[package]] 315 | name = "futures-channel" 316 | version = "0.3.31" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 319 | dependencies = [ 320 | "futures-core", 321 | "futures-sink", 322 | ] 323 | 324 | [[package]] 325 | name = "futures-core" 326 | version = "0.3.31" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 329 | 330 | [[package]] 331 | name = "futures-executor" 332 | version = "0.3.31" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 335 | dependencies = [ 336 | "futures-core", 337 | "futures-task", 338 | "futures-util", 339 | ] 340 | 341 | [[package]] 342 | name = "futures-io" 343 | version = "0.3.31" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 346 | 347 | [[package]] 348 | name = "futures-macro" 349 | version = "0.3.31" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 352 | dependencies = [ 353 | "proc-macro2", 354 | "quote", 355 | "syn 2.0.111", 356 | ] 357 | 358 | [[package]] 359 | name = "futures-sink" 360 | version = "0.3.31" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 363 | 364 | [[package]] 365 | name = "futures-task" 366 | version = "0.3.31" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 369 | 370 | [[package]] 371 | name = "futures-util" 372 | version = "0.3.31" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 375 | dependencies = [ 376 | "futures-channel", 377 | "futures-core", 378 | "futures-io", 379 | "futures-macro", 380 | "futures-sink", 381 | "futures-task", 382 | "memchr", 383 | "pin-project-lite", 384 | "pin-utils", 385 | "slab", 386 | ] 387 | 388 | [[package]] 389 | name = "getrandom" 390 | version = "0.2.16" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 393 | dependencies = [ 394 | "cfg-if", 395 | "libc", 396 | "wasi", 397 | ] 398 | 399 | [[package]] 400 | name = "getrandom" 401 | version = "0.3.4" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 404 | dependencies = [ 405 | "cfg-if", 406 | "libc", 407 | "r-efi", 408 | "wasip2", 409 | ] 410 | 411 | [[package]] 412 | name = "h2" 413 | version = "0.3.27" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" 416 | dependencies = [ 417 | "bytes", 418 | "fnv", 419 | "futures-core", 420 | "futures-sink", 421 | "futures-util", 422 | "http 0.2.12", 423 | "indexmap 2.12.1", 424 | "slab", 425 | "tokio", 426 | "tokio-util", 427 | "tracing", 428 | ] 429 | 430 | [[package]] 431 | name = "hashbrown" 432 | version = "0.12.3" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 435 | 436 | [[package]] 437 | name = "hashbrown" 438 | version = "0.15.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 441 | dependencies = [ 442 | "foldhash", 443 | ] 444 | 445 | [[package]] 446 | name = "hashbrown" 447 | version = "0.16.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 450 | 451 | [[package]] 452 | name = "hashlink" 453 | version = "0.10.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 456 | dependencies = [ 457 | "hashbrown 0.15.5", 458 | ] 459 | 460 | [[package]] 461 | name = "http" 462 | version = "0.2.12" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 465 | dependencies = [ 466 | "bytes", 467 | "fnv", 468 | "itoa", 469 | ] 470 | 471 | [[package]] 472 | name = "http" 473 | version = "1.4.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 476 | dependencies = [ 477 | "bytes", 478 | "itoa", 479 | ] 480 | 481 | [[package]] 482 | name = "http-body" 483 | version = "0.4.6" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 486 | dependencies = [ 487 | "bytes", 488 | "http 0.2.12", 489 | "pin-project-lite", 490 | ] 491 | 492 | [[package]] 493 | name = "http-body" 494 | version = "1.0.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 497 | dependencies = [ 498 | "bytes", 499 | "http 1.4.0", 500 | ] 501 | 502 | [[package]] 503 | name = "http-body-util" 504 | version = "0.1.3" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 507 | dependencies = [ 508 | "bytes", 509 | "futures-core", 510 | "http 1.4.0", 511 | "http-body 1.0.1", 512 | "pin-project-lite", 513 | ] 514 | 515 | [[package]] 516 | name = "httparse" 517 | version = "1.10.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 520 | 521 | [[package]] 522 | name = "httpdate" 523 | version = "1.0.3" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 526 | 527 | [[package]] 528 | name = "hyper" 529 | version = "0.14.32" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 532 | dependencies = [ 533 | "bytes", 534 | "futures-channel", 535 | "futures-core", 536 | "futures-util", 537 | "h2", 538 | "http 0.2.12", 539 | "http-body 0.4.6", 540 | "httparse", 541 | "httpdate", 542 | "itoa", 543 | "pin-project-lite", 544 | "socket2 0.5.10", 545 | "tokio", 546 | "tower-service", 547 | "tracing", 548 | "want", 549 | ] 550 | 551 | [[package]] 552 | name = "hyper" 553 | version = "1.8.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 556 | dependencies = [ 557 | "bytes", 558 | "http 1.4.0", 559 | "http-body 1.0.1", 560 | "pin-project-lite", 561 | "smallvec", 562 | "tokio", 563 | "want", 564 | ] 565 | 566 | [[package]] 567 | name = "hyper-timeout" 568 | version = "0.4.1" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" 571 | dependencies = [ 572 | "hyper 0.14.32", 573 | "pin-project-lite", 574 | "tokio", 575 | "tokio-io-timeout", 576 | ] 577 | 578 | [[package]] 579 | name = "hyper-tls" 580 | version = "0.5.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 583 | dependencies = [ 584 | "bytes", 585 | "hyper 0.14.32", 586 | "native-tls", 587 | "tokio", 588 | "tokio-native-tls", 589 | ] 590 | 591 | [[package]] 592 | name = "hyper-tls" 593 | version = "0.6.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 596 | dependencies = [ 597 | "bytes", 598 | "http-body-util", 599 | "hyper 1.8.1", 600 | "hyper-util", 601 | "native-tls", 602 | "tokio", 603 | "tokio-native-tls", 604 | "tower-service", 605 | ] 606 | 607 | [[package]] 608 | name = "hyper-util" 609 | version = "0.1.18" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 612 | dependencies = [ 613 | "bytes", 614 | "futures-channel", 615 | "futures-core", 616 | "futures-util", 617 | "http 1.4.0", 618 | "http-body 1.0.1", 619 | "hyper 1.8.1", 620 | "libc", 621 | "pin-project-lite", 622 | "socket2 0.6.1", 623 | "tokio", 624 | "tower-service", 625 | "tracing", 626 | ] 627 | 628 | [[package]] 629 | name = "iana-time-zone" 630 | version = "0.1.64" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 633 | dependencies = [ 634 | "android_system_properties", 635 | "core-foundation-sys", 636 | "iana-time-zone-haiku", 637 | "js-sys", 638 | "log", 639 | "wasm-bindgen", 640 | "windows-core", 641 | ] 642 | 643 | [[package]] 644 | name = "iana-time-zone-haiku" 645 | version = "0.1.2" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 648 | dependencies = [ 649 | "cc", 650 | ] 651 | 652 | [[package]] 653 | name = "icu_collections" 654 | version = "2.1.1" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 657 | dependencies = [ 658 | "displaydoc", 659 | "potential_utf", 660 | "yoke", 661 | "zerofrom", 662 | "zerovec", 663 | ] 664 | 665 | [[package]] 666 | name = "icu_locale_core" 667 | version = "2.1.1" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 670 | dependencies = [ 671 | "displaydoc", 672 | "litemap", 673 | "tinystr", 674 | "writeable", 675 | "zerovec", 676 | ] 677 | 678 | [[package]] 679 | name = "icu_normalizer" 680 | version = "2.1.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 683 | dependencies = [ 684 | "icu_collections", 685 | "icu_normalizer_data", 686 | "icu_properties", 687 | "icu_provider", 688 | "smallvec", 689 | "zerovec", 690 | ] 691 | 692 | [[package]] 693 | name = "icu_normalizer_data" 694 | version = "2.1.1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 697 | 698 | [[package]] 699 | name = "icu_properties" 700 | version = "2.1.1" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 703 | dependencies = [ 704 | "icu_collections", 705 | "icu_locale_core", 706 | "icu_properties_data", 707 | "icu_provider", 708 | "zerotrie", 709 | "zerovec", 710 | ] 711 | 712 | [[package]] 713 | name = "icu_properties_data" 714 | version = "2.1.1" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 717 | 718 | [[package]] 719 | name = "icu_provider" 720 | version = "2.1.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 723 | dependencies = [ 724 | "displaydoc", 725 | "icu_locale_core", 726 | "writeable", 727 | "yoke", 728 | "zerofrom", 729 | "zerotrie", 730 | "zerovec", 731 | ] 732 | 733 | [[package]] 734 | name = "idna" 735 | version = "1.1.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 738 | dependencies = [ 739 | "idna_adapter", 740 | "smallvec", 741 | "utf8_iter", 742 | ] 743 | 744 | [[package]] 745 | name = "idna_adapter" 746 | version = "1.2.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 749 | dependencies = [ 750 | "icu_normalizer", 751 | "icu_properties", 752 | ] 753 | 754 | [[package]] 755 | name = "indexmap" 756 | version = "1.9.3" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 759 | dependencies = [ 760 | "autocfg", 761 | "hashbrown 0.12.3", 762 | ] 763 | 764 | [[package]] 765 | name = "indexmap" 766 | version = "2.12.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 769 | dependencies = [ 770 | "equivalent", 771 | "hashbrown 0.16.1", 772 | ] 773 | 774 | [[package]] 775 | name = "itoa" 776 | version = "1.0.15" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 779 | 780 | [[package]] 781 | name = "js-sys" 782 | version = "0.3.83" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 785 | dependencies = [ 786 | "once_cell", 787 | "wasm-bindgen", 788 | ] 789 | 790 | [[package]] 791 | name = "libc" 792 | version = "0.2.177" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 795 | 796 | [[package]] 797 | name = "libsqlite3-sys" 798 | version = "0.35.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" 801 | dependencies = [ 802 | "pkg-config", 803 | "vcpkg", 804 | ] 805 | 806 | [[package]] 807 | name = "linux-raw-sys" 808 | version = "0.11.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 811 | 812 | [[package]] 813 | name = "litemap" 814 | version = "0.8.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 817 | 818 | [[package]] 819 | name = "lock_api" 820 | version = "0.4.14" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 823 | dependencies = [ 824 | "scopeguard", 825 | ] 826 | 827 | [[package]] 828 | name = "log" 829 | version = "0.4.28" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 832 | 833 | [[package]] 834 | name = "matchit" 835 | version = "0.7.3" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 838 | 839 | [[package]] 840 | name = "memchr" 841 | version = "2.7.6" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 844 | 845 | [[package]] 846 | name = "mime" 847 | version = "0.3.17" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 850 | 851 | [[package]] 852 | name = "mio" 853 | version = "1.1.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 856 | dependencies = [ 857 | "libc", 858 | "wasi", 859 | "windows-sys 0.61.2", 860 | ] 861 | 862 | [[package]] 863 | name = "native-tls" 864 | version = "0.2.14" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 867 | dependencies = [ 868 | "libc", 869 | "log", 870 | "openssl", 871 | "openssl-probe", 872 | "openssl-sys", 873 | "schannel", 874 | "security-framework", 875 | "security-framework-sys", 876 | "tempfile", 877 | ] 878 | 879 | [[package]] 880 | name = "num-conv" 881 | version = "0.1.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 884 | 885 | [[package]] 886 | name = "num-traits" 887 | version = "0.2.19" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 890 | dependencies = [ 891 | "autocfg", 892 | ] 893 | 894 | [[package]] 895 | name = "num_threads" 896 | version = "0.1.7" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 899 | dependencies = [ 900 | "libc", 901 | ] 902 | 903 | [[package]] 904 | name = "once_cell" 905 | version = "1.21.3" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 908 | 909 | [[package]] 910 | name = "openssl" 911 | version = "0.10.75" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 914 | dependencies = [ 915 | "bitflags 2.10.0", 916 | "cfg-if", 917 | "foreign-types", 918 | "libc", 919 | "once_cell", 920 | "openssl-macros", 921 | "openssl-sys", 922 | ] 923 | 924 | [[package]] 925 | name = "openssl-macros" 926 | version = "0.1.1" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 929 | dependencies = [ 930 | "proc-macro2", 931 | "quote", 932 | "syn 2.0.111", 933 | ] 934 | 935 | [[package]] 936 | name = "openssl-probe" 937 | version = "0.1.6" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 940 | 941 | [[package]] 942 | name = "openssl-sys" 943 | version = "0.9.111" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 946 | dependencies = [ 947 | "cc", 948 | "libc", 949 | "pkg-config", 950 | "vcpkg", 951 | ] 952 | 953 | [[package]] 954 | name = "parking_lot" 955 | version = "0.12.5" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 958 | dependencies = [ 959 | "lock_api", 960 | "parking_lot_core", 961 | ] 962 | 963 | [[package]] 964 | name = "parking_lot_core" 965 | version = "0.9.12" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 968 | dependencies = [ 969 | "cfg-if", 970 | "libc", 971 | "redox_syscall", 972 | "smallvec", 973 | "windows-link", 974 | ] 975 | 976 | [[package]] 977 | name = "paste" 978 | version = "1.0.15" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 981 | 982 | [[package]] 983 | name = "percent-encoding" 984 | version = "2.3.2" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 987 | 988 | [[package]] 989 | name = "pin-project" 990 | version = "1.1.10" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 993 | dependencies = [ 994 | "pin-project-internal", 995 | ] 996 | 997 | [[package]] 998 | name = "pin-project-internal" 999 | version = "1.1.10" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 1002 | dependencies = [ 1003 | "proc-macro2", 1004 | "quote", 1005 | "syn 2.0.111", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "pin-project-lite" 1010 | version = "0.2.16" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1013 | 1014 | [[package]] 1015 | name = "pin-utils" 1016 | version = "0.1.0" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1019 | 1020 | [[package]] 1021 | name = "pkg-config" 1022 | version = "0.3.32" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1025 | 1026 | [[package]] 1027 | name = "potential_utf" 1028 | version = "0.1.4" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1031 | dependencies = [ 1032 | "zerovec", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "powerfmt" 1037 | version = "0.2.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1040 | 1041 | [[package]] 1042 | name = "ppv-lite86" 1043 | version = "0.2.21" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1046 | dependencies = [ 1047 | "zerocopy", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "proc-macro2" 1052 | version = "1.0.103" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 1055 | dependencies = [ 1056 | "unicode-ident", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "prost" 1061 | version = "0.11.9" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" 1064 | dependencies = [ 1065 | "bytes", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "quote" 1070 | version = "1.0.42" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 1073 | dependencies = [ 1074 | "proc-macro2", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "r-efi" 1079 | version = "5.3.0" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1082 | 1083 | [[package]] 1084 | name = "r2d2" 1085 | version = "0.8.10" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" 1088 | dependencies = [ 1089 | "log", 1090 | "parking_lot", 1091 | "scheduled-thread-pool", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "r2d2_sqlite" 1096 | version = "0.31.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "63417e83dc891797eea3ad379f52a5986da4bca0d6ef28baf4d14034dd111b0c" 1099 | dependencies = [ 1100 | "r2d2", 1101 | "rusqlite", 1102 | "uuid", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "rand" 1107 | version = "0.8.5" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1110 | dependencies = [ 1111 | "libc", 1112 | "rand_chacha 0.3.1", 1113 | "rand_core 0.6.4", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "rand" 1118 | version = "0.9.2" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1121 | dependencies = [ 1122 | "rand_chacha 0.9.0", 1123 | "rand_core 0.9.3", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "rand_chacha" 1128 | version = "0.3.1" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1131 | dependencies = [ 1132 | "ppv-lite86", 1133 | "rand_core 0.6.4", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "rand_chacha" 1138 | version = "0.9.0" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1141 | dependencies = [ 1142 | "ppv-lite86", 1143 | "rand_core 0.9.3", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "rand_core" 1148 | version = "0.6.4" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1151 | dependencies = [ 1152 | "getrandom 0.2.16", 1153 | ] 1154 | 1155 | [[package]] 1156 | name = "rand_core" 1157 | version = "0.9.3" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1160 | dependencies = [ 1161 | "getrandom 0.3.4", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "redox_syscall" 1166 | version = "0.5.18" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1169 | dependencies = [ 1170 | "bitflags 2.10.0", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "regex" 1175 | version = "1.12.2" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 1178 | dependencies = [ 1179 | "aho-corasick", 1180 | "memchr", 1181 | "regex-automata", 1182 | "regex-syntax", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "regex-automata" 1187 | version = "0.4.13" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 1190 | dependencies = [ 1191 | "aho-corasick", 1192 | "memchr", 1193 | "regex-syntax", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "regex-syntax" 1198 | version = "0.8.8" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 1201 | 1202 | [[package]] 1203 | name = "ring" 1204 | version = "0.17.14" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1207 | dependencies = [ 1208 | "cc", 1209 | "cfg-if", 1210 | "getrandom 0.2.16", 1211 | "libc", 1212 | "untrusted", 1213 | "windows-sys 0.52.0", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "rusqlite" 1218 | version = "0.37.0" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" 1221 | dependencies = [ 1222 | "bitflags 2.10.0", 1223 | "chrono", 1224 | "fallible-iterator", 1225 | "fallible-streaming-iterator", 1226 | "hashlink", 1227 | "libsqlite3-sys", 1228 | "smallvec", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "rustix" 1233 | version = "1.1.2" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 1236 | dependencies = [ 1237 | "bitflags 2.10.0", 1238 | "errno", 1239 | "libc", 1240 | "linux-raw-sys", 1241 | "windows-sys 0.61.2", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "rustls" 1246 | version = "0.21.12" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 1249 | dependencies = [ 1250 | "log", 1251 | "ring", 1252 | "rustls-webpki", 1253 | "sct", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "rustls-native-certs" 1258 | version = "0.6.3" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" 1261 | dependencies = [ 1262 | "openssl-probe", 1263 | "rustls-pemfile", 1264 | "schannel", 1265 | "security-framework", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "rustls-pemfile" 1270 | version = "1.0.4" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1273 | dependencies = [ 1274 | "base64", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "rustls-webpki" 1279 | version = "0.101.7" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 1282 | dependencies = [ 1283 | "ring", 1284 | "untrusted", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "rustversion" 1289 | version = "1.0.22" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1292 | 1293 | [[package]] 1294 | name = "ryu" 1295 | version = "1.0.20" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1298 | 1299 | [[package]] 1300 | name = "schannel" 1301 | version = "0.1.28" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 1304 | dependencies = [ 1305 | "windows-sys 0.61.2", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "scheduled-thread-pool" 1310 | version = "0.2.7" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" 1313 | dependencies = [ 1314 | "parking_lot", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "scopeguard" 1319 | version = "1.2.0" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1322 | 1323 | [[package]] 1324 | name = "sct" 1325 | version = "0.7.1" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 1328 | dependencies = [ 1329 | "ring", 1330 | "untrusted", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "security-framework" 1335 | version = "2.11.1" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1338 | dependencies = [ 1339 | "bitflags 2.10.0", 1340 | "core-foundation", 1341 | "core-foundation-sys", 1342 | "libc", 1343 | "security-framework-sys", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "security-framework-sys" 1348 | version = "2.15.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 1351 | dependencies = [ 1352 | "core-foundation-sys", 1353 | "libc", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "serde" 1358 | version = "1.0.228" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1361 | dependencies = [ 1362 | "serde_core", 1363 | "serde_derive", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "serde_core" 1368 | version = "1.0.228" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1371 | dependencies = [ 1372 | "serde_derive", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "serde_derive" 1377 | version = "1.0.228" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1380 | dependencies = [ 1381 | "proc-macro2", 1382 | "quote", 1383 | "syn 2.0.111", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "serde_json" 1388 | version = "1.0.145" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 1391 | dependencies = [ 1392 | "itoa", 1393 | "memchr", 1394 | "ryu", 1395 | "serde", 1396 | "serde_core", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "shlex" 1401 | version = "1.3.0" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1404 | 1405 | [[package]] 1406 | name = "signal-hook-registry" 1407 | version = "1.4.7" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 1410 | dependencies = [ 1411 | "libc", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "simple_logger" 1416 | version = "5.1.0" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "291bee647ce7310b0ea721bfd7e0525517b4468eb7c7e15eb8bd774343179702" 1419 | dependencies = [ 1420 | "colored", 1421 | "log", 1422 | "time", 1423 | "windows-sys 0.61.2", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "slab" 1428 | version = "0.4.11" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1431 | 1432 | [[package]] 1433 | name = "smallvec" 1434 | version = "1.15.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1437 | 1438 | [[package]] 1439 | name = "socket2" 1440 | version = "0.5.10" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1443 | dependencies = [ 1444 | "libc", 1445 | "windows-sys 0.52.0", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "socket2" 1450 | version = "0.6.1" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 1453 | dependencies = [ 1454 | "libc", 1455 | "windows-sys 0.60.2", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "stable_deref_trait" 1460 | version = "1.2.1" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1463 | 1464 | [[package]] 1465 | name = "syn" 1466 | version = "1.0.109" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1469 | dependencies = [ 1470 | "proc-macro2", 1471 | "quote", 1472 | "unicode-ident", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "syn" 1477 | version = "2.0.111" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 1480 | dependencies = [ 1481 | "proc-macro2", 1482 | "quote", 1483 | "unicode-ident", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "sync_wrapper" 1488 | version = "0.1.2" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1491 | 1492 | [[package]] 1493 | name = "synstructure" 1494 | version = "0.13.2" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1497 | dependencies = [ 1498 | "proc-macro2", 1499 | "quote", 1500 | "syn 2.0.111", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "tabular" 1505 | version = "0.2.0" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "d9a2882c514780a1973df90de9d68adcd8871bacc9a6331c3f28e6d2ff91a3d1" 1508 | dependencies = [ 1509 | "unicode-width", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "telegram-raf" 1514 | version = "0.1.3" 1515 | dependencies = [ 1516 | "chrono", 1517 | "data-encoding", 1518 | "default", 1519 | "hyper 1.8.1", 1520 | "hyper-tls 0.6.0", 1521 | "log", 1522 | "r2d2", 1523 | "r2d2_sqlite", 1524 | "regex", 1525 | "rusqlite", 1526 | "simple_logger", 1527 | "tabular", 1528 | "telexide-fork", 1529 | "tokio", 1530 | "typemap", 1531 | "url", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "telexide-fork" 1536 | version = "0.2.5" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "8ba5080c7b152c18ac6c95a2216d32adf7d70bb69abb23f2223072990c222b14" 1539 | dependencies = [ 1540 | "async-trait", 1541 | "chrono", 1542 | "futures", 1543 | "http 0.2.12", 1544 | "hyper 0.14.32", 1545 | "hyper-tls 0.5.0", 1546 | "log", 1547 | "parking_lot", 1548 | "paste", 1549 | "serde", 1550 | "serde_json", 1551 | "telexide_fork_proc_macros", 1552 | "tokio", 1553 | "tonic", 1554 | "typemap", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "telexide_fork_proc_macros" 1559 | version = "0.1.1" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "4bb8d4dd6cd29f2472338a3ce9ea88f29973e8264f662a81ecd7eb5a7a8188c2" 1562 | dependencies = [ 1563 | "proc-macro2", 1564 | "quote", 1565 | "syn 1.0.109", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "tempfile" 1570 | version = "3.23.0" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 1573 | dependencies = [ 1574 | "fastrand", 1575 | "getrandom 0.3.4", 1576 | "once_cell", 1577 | "rustix", 1578 | "windows-sys 0.61.2", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "time" 1583 | version = "0.3.44" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 1586 | dependencies = [ 1587 | "deranged", 1588 | "itoa", 1589 | "libc", 1590 | "num-conv", 1591 | "num_threads", 1592 | "powerfmt", 1593 | "serde", 1594 | "time-core", 1595 | "time-macros", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "time-core" 1600 | version = "0.1.6" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 1603 | 1604 | [[package]] 1605 | name = "time-macros" 1606 | version = "0.2.24" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 1609 | dependencies = [ 1610 | "num-conv", 1611 | "time-core", 1612 | ] 1613 | 1614 | [[package]] 1615 | name = "tinystr" 1616 | version = "0.8.2" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1619 | dependencies = [ 1620 | "displaydoc", 1621 | "zerovec", 1622 | ] 1623 | 1624 | [[package]] 1625 | name = "tokio" 1626 | version = "1.48.0" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1629 | dependencies = [ 1630 | "bytes", 1631 | "libc", 1632 | "mio", 1633 | "parking_lot", 1634 | "pin-project-lite", 1635 | "signal-hook-registry", 1636 | "socket2 0.6.1", 1637 | "tokio-macros", 1638 | "windows-sys 0.61.2", 1639 | ] 1640 | 1641 | [[package]] 1642 | name = "tokio-io-timeout" 1643 | version = "1.2.1" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" 1646 | dependencies = [ 1647 | "pin-project-lite", 1648 | "tokio", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "tokio-macros" 1653 | version = "2.6.0" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1656 | dependencies = [ 1657 | "proc-macro2", 1658 | "quote", 1659 | "syn 2.0.111", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "tokio-native-tls" 1664 | version = "0.3.1" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1667 | dependencies = [ 1668 | "native-tls", 1669 | "tokio", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "tokio-rustls" 1674 | version = "0.24.1" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 1677 | dependencies = [ 1678 | "rustls", 1679 | "tokio", 1680 | ] 1681 | 1682 | [[package]] 1683 | name = "tokio-stream" 1684 | version = "0.1.17" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 1687 | dependencies = [ 1688 | "futures-core", 1689 | "pin-project-lite", 1690 | "tokio", 1691 | ] 1692 | 1693 | [[package]] 1694 | name = "tokio-util" 1695 | version = "0.7.17" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 1698 | dependencies = [ 1699 | "bytes", 1700 | "futures-core", 1701 | "futures-sink", 1702 | "pin-project-lite", 1703 | "tokio", 1704 | ] 1705 | 1706 | [[package]] 1707 | name = "tonic" 1708 | version = "0.9.2" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" 1711 | dependencies = [ 1712 | "async-stream", 1713 | "async-trait", 1714 | "axum", 1715 | "base64", 1716 | "bytes", 1717 | "futures-core", 1718 | "futures-util", 1719 | "h2", 1720 | "http 0.2.12", 1721 | "http-body 0.4.6", 1722 | "hyper 0.14.32", 1723 | "hyper-timeout", 1724 | "percent-encoding", 1725 | "pin-project", 1726 | "prost", 1727 | "rustls-native-certs", 1728 | "rustls-pemfile", 1729 | "tokio", 1730 | "tokio-rustls", 1731 | "tokio-stream", 1732 | "tower", 1733 | "tower-layer", 1734 | "tower-service", 1735 | "tracing", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "tower" 1740 | version = "0.4.13" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1743 | dependencies = [ 1744 | "futures-core", 1745 | "futures-util", 1746 | "indexmap 1.9.3", 1747 | "pin-project", 1748 | "pin-project-lite", 1749 | "rand 0.8.5", 1750 | "slab", 1751 | "tokio", 1752 | "tokio-util", 1753 | "tower-layer", 1754 | "tower-service", 1755 | "tracing", 1756 | ] 1757 | 1758 | [[package]] 1759 | name = "tower-layer" 1760 | version = "0.3.3" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1763 | 1764 | [[package]] 1765 | name = "tower-service" 1766 | version = "0.3.3" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1769 | 1770 | [[package]] 1771 | name = "tracing" 1772 | version = "0.1.43" 1773 | source = "registry+https://github.com/rust-lang/crates.io-index" 1774 | checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" 1775 | dependencies = [ 1776 | "pin-project-lite", 1777 | "tracing-attributes", 1778 | "tracing-core", 1779 | ] 1780 | 1781 | [[package]] 1782 | name = "tracing-attributes" 1783 | version = "0.1.31" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 1786 | dependencies = [ 1787 | "proc-macro2", 1788 | "quote", 1789 | "syn 2.0.111", 1790 | ] 1791 | 1792 | [[package]] 1793 | name = "tracing-core" 1794 | version = "0.1.35" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" 1797 | dependencies = [ 1798 | "once_cell", 1799 | ] 1800 | 1801 | [[package]] 1802 | name = "traitobject" 1803 | version = "0.1.1" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "04a79e25382e2e852e8da874249358d382ebaf259d0d34e75d8db16a7efabbc7" 1806 | 1807 | [[package]] 1808 | name = "try-lock" 1809 | version = "0.2.5" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1812 | 1813 | [[package]] 1814 | name = "typemap" 1815 | version = "0.3.3" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" 1818 | dependencies = [ 1819 | "unsafe-any", 1820 | ] 1821 | 1822 | [[package]] 1823 | name = "unicode-ident" 1824 | version = "1.0.22" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1827 | 1828 | [[package]] 1829 | name = "unicode-width" 1830 | version = "0.1.14" 1831 | source = "registry+https://github.com/rust-lang/crates.io-index" 1832 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1833 | 1834 | [[package]] 1835 | name = "unsafe-any" 1836 | version = "0.4.2" 1837 | source = "registry+https://github.com/rust-lang/crates.io-index" 1838 | checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" 1839 | dependencies = [ 1840 | "traitobject", 1841 | ] 1842 | 1843 | [[package]] 1844 | name = "untrusted" 1845 | version = "0.9.0" 1846 | source = "registry+https://github.com/rust-lang/crates.io-index" 1847 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1848 | 1849 | [[package]] 1850 | name = "url" 1851 | version = "2.5.7" 1852 | source = "registry+https://github.com/rust-lang/crates.io-index" 1853 | checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1854 | dependencies = [ 1855 | "form_urlencoded", 1856 | "idna", 1857 | "percent-encoding", 1858 | "serde", 1859 | ] 1860 | 1861 | [[package]] 1862 | name = "utf8_iter" 1863 | version = "1.0.4" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1866 | 1867 | [[package]] 1868 | name = "uuid" 1869 | version = "1.18.1" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 1872 | dependencies = [ 1873 | "getrandom 0.3.4", 1874 | "js-sys", 1875 | "rand 0.9.2", 1876 | "wasm-bindgen", 1877 | ] 1878 | 1879 | [[package]] 1880 | name = "vcpkg" 1881 | version = "0.2.15" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1884 | 1885 | [[package]] 1886 | name = "want" 1887 | version = "0.3.1" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1890 | dependencies = [ 1891 | "try-lock", 1892 | ] 1893 | 1894 | [[package]] 1895 | name = "wasi" 1896 | version = "0.11.1+wasi-snapshot-preview1" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1899 | 1900 | [[package]] 1901 | name = "wasip2" 1902 | version = "1.0.1+wasi-0.2.4" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1905 | dependencies = [ 1906 | "wit-bindgen", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "wasm-bindgen" 1911 | version = "0.2.106" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 1914 | dependencies = [ 1915 | "cfg-if", 1916 | "once_cell", 1917 | "rustversion", 1918 | "wasm-bindgen-macro", 1919 | "wasm-bindgen-shared", 1920 | ] 1921 | 1922 | [[package]] 1923 | name = "wasm-bindgen-macro" 1924 | version = "0.2.106" 1925 | source = "registry+https://github.com/rust-lang/crates.io-index" 1926 | checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 1927 | dependencies = [ 1928 | "quote", 1929 | "wasm-bindgen-macro-support", 1930 | ] 1931 | 1932 | [[package]] 1933 | name = "wasm-bindgen-macro-support" 1934 | version = "0.2.106" 1935 | source = "registry+https://github.com/rust-lang/crates.io-index" 1936 | checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 1937 | dependencies = [ 1938 | "bumpalo", 1939 | "proc-macro2", 1940 | "quote", 1941 | "syn 2.0.111", 1942 | "wasm-bindgen-shared", 1943 | ] 1944 | 1945 | [[package]] 1946 | name = "wasm-bindgen-shared" 1947 | version = "0.2.106" 1948 | source = "registry+https://github.com/rust-lang/crates.io-index" 1949 | checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 1950 | dependencies = [ 1951 | "unicode-ident", 1952 | ] 1953 | 1954 | [[package]] 1955 | name = "windows-core" 1956 | version = "0.62.2" 1957 | source = "registry+https://github.com/rust-lang/crates.io-index" 1958 | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 1959 | dependencies = [ 1960 | "windows-implement", 1961 | "windows-interface", 1962 | "windows-link", 1963 | "windows-result", 1964 | "windows-strings", 1965 | ] 1966 | 1967 | [[package]] 1968 | name = "windows-implement" 1969 | version = "0.60.2" 1970 | source = "registry+https://github.com/rust-lang/crates.io-index" 1971 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 1972 | dependencies = [ 1973 | "proc-macro2", 1974 | "quote", 1975 | "syn 2.0.111", 1976 | ] 1977 | 1978 | [[package]] 1979 | name = "windows-interface" 1980 | version = "0.59.3" 1981 | source = "registry+https://github.com/rust-lang/crates.io-index" 1982 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 1983 | dependencies = [ 1984 | "proc-macro2", 1985 | "quote", 1986 | "syn 2.0.111", 1987 | ] 1988 | 1989 | [[package]] 1990 | name = "windows-link" 1991 | version = "0.2.1" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1994 | 1995 | [[package]] 1996 | name = "windows-result" 1997 | version = "0.4.1" 1998 | source = "registry+https://github.com/rust-lang/crates.io-index" 1999 | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 2000 | dependencies = [ 2001 | "windows-link", 2002 | ] 2003 | 2004 | [[package]] 2005 | name = "windows-strings" 2006 | version = "0.5.1" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 2009 | dependencies = [ 2010 | "windows-link", 2011 | ] 2012 | 2013 | [[package]] 2014 | name = "windows-sys" 2015 | version = "0.52.0" 2016 | source = "registry+https://github.com/rust-lang/crates.io-index" 2017 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2018 | dependencies = [ 2019 | "windows-targets 0.52.6", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "windows-sys" 2024 | version = "0.59.0" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2027 | dependencies = [ 2028 | "windows-targets 0.52.6", 2029 | ] 2030 | 2031 | [[package]] 2032 | name = "windows-sys" 2033 | version = "0.60.2" 2034 | source = "registry+https://github.com/rust-lang/crates.io-index" 2035 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 2036 | dependencies = [ 2037 | "windows-targets 0.53.5", 2038 | ] 2039 | 2040 | [[package]] 2041 | name = "windows-sys" 2042 | version = "0.61.2" 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" 2044 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 2045 | dependencies = [ 2046 | "windows-link", 2047 | ] 2048 | 2049 | [[package]] 2050 | name = "windows-targets" 2051 | version = "0.52.6" 2052 | source = "registry+https://github.com/rust-lang/crates.io-index" 2053 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2054 | dependencies = [ 2055 | "windows_aarch64_gnullvm 0.52.6", 2056 | "windows_aarch64_msvc 0.52.6", 2057 | "windows_i686_gnu 0.52.6", 2058 | "windows_i686_gnullvm 0.52.6", 2059 | "windows_i686_msvc 0.52.6", 2060 | "windows_x86_64_gnu 0.52.6", 2061 | "windows_x86_64_gnullvm 0.52.6", 2062 | "windows_x86_64_msvc 0.52.6", 2063 | ] 2064 | 2065 | [[package]] 2066 | name = "windows-targets" 2067 | version = "0.53.5" 2068 | source = "registry+https://github.com/rust-lang/crates.io-index" 2069 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 2070 | dependencies = [ 2071 | "windows-link", 2072 | "windows_aarch64_gnullvm 0.53.1", 2073 | "windows_aarch64_msvc 0.53.1", 2074 | "windows_i686_gnu 0.53.1", 2075 | "windows_i686_gnullvm 0.53.1", 2076 | "windows_i686_msvc 0.53.1", 2077 | "windows_x86_64_gnu 0.53.1", 2078 | "windows_x86_64_gnullvm 0.53.1", 2079 | "windows_x86_64_msvc 0.53.1", 2080 | ] 2081 | 2082 | [[package]] 2083 | name = "windows_aarch64_gnullvm" 2084 | version = "0.52.6" 2085 | source = "registry+https://github.com/rust-lang/crates.io-index" 2086 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2087 | 2088 | [[package]] 2089 | name = "windows_aarch64_gnullvm" 2090 | version = "0.53.1" 2091 | source = "registry+https://github.com/rust-lang/crates.io-index" 2092 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 2093 | 2094 | [[package]] 2095 | name = "windows_aarch64_msvc" 2096 | version = "0.52.6" 2097 | source = "registry+https://github.com/rust-lang/crates.io-index" 2098 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2099 | 2100 | [[package]] 2101 | name = "windows_aarch64_msvc" 2102 | version = "0.53.1" 2103 | source = "registry+https://github.com/rust-lang/crates.io-index" 2104 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 2105 | 2106 | [[package]] 2107 | name = "windows_i686_gnu" 2108 | version = "0.52.6" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2111 | 2112 | [[package]] 2113 | name = "windows_i686_gnu" 2114 | version = "0.53.1" 2115 | source = "registry+https://github.com/rust-lang/crates.io-index" 2116 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 2117 | 2118 | [[package]] 2119 | name = "windows_i686_gnullvm" 2120 | version = "0.52.6" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2123 | 2124 | [[package]] 2125 | name = "windows_i686_gnullvm" 2126 | version = "0.53.1" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 2129 | 2130 | [[package]] 2131 | name = "windows_i686_msvc" 2132 | version = "0.52.6" 2133 | source = "registry+https://github.com/rust-lang/crates.io-index" 2134 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2135 | 2136 | [[package]] 2137 | name = "windows_i686_msvc" 2138 | version = "0.53.1" 2139 | source = "registry+https://github.com/rust-lang/crates.io-index" 2140 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 2141 | 2142 | [[package]] 2143 | name = "windows_x86_64_gnu" 2144 | version = "0.52.6" 2145 | source = "registry+https://github.com/rust-lang/crates.io-index" 2146 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2147 | 2148 | [[package]] 2149 | name = "windows_x86_64_gnu" 2150 | version = "0.53.1" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 2153 | 2154 | [[package]] 2155 | name = "windows_x86_64_gnullvm" 2156 | version = "0.52.6" 2157 | source = "registry+https://github.com/rust-lang/crates.io-index" 2158 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2159 | 2160 | [[package]] 2161 | name = "windows_x86_64_gnullvm" 2162 | version = "0.53.1" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 2165 | 2166 | [[package]] 2167 | name = "windows_x86_64_msvc" 2168 | version = "0.52.6" 2169 | source = "registry+https://github.com/rust-lang/crates.io-index" 2170 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2171 | 2172 | [[package]] 2173 | name = "windows_x86_64_msvc" 2174 | version = "0.53.1" 2175 | source = "registry+https://github.com/rust-lang/crates.io-index" 2176 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 2177 | 2178 | [[package]] 2179 | name = "wit-bindgen" 2180 | version = "0.46.0" 2181 | source = "registry+https://github.com/rust-lang/crates.io-index" 2182 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 2183 | 2184 | [[package]] 2185 | name = "writeable" 2186 | version = "0.6.2" 2187 | source = "registry+https://github.com/rust-lang/crates.io-index" 2188 | checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 2189 | 2190 | [[package]] 2191 | name = "yoke" 2192 | version = "0.8.1" 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" 2194 | checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 2195 | dependencies = [ 2196 | "stable_deref_trait", 2197 | "yoke-derive", 2198 | "zerofrom", 2199 | ] 2200 | 2201 | [[package]] 2202 | name = "yoke-derive" 2203 | version = "0.8.1" 2204 | source = "registry+https://github.com/rust-lang/crates.io-index" 2205 | checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 2206 | dependencies = [ 2207 | "proc-macro2", 2208 | "quote", 2209 | "syn 2.0.111", 2210 | "synstructure", 2211 | ] 2212 | 2213 | [[package]] 2214 | name = "zerocopy" 2215 | version = "0.8.31" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 2218 | dependencies = [ 2219 | "zerocopy-derive", 2220 | ] 2221 | 2222 | [[package]] 2223 | name = "zerocopy-derive" 2224 | version = "0.8.31" 2225 | source = "registry+https://github.com/rust-lang/crates.io-index" 2226 | checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 2227 | dependencies = [ 2228 | "proc-macro2", 2229 | "quote", 2230 | "syn 2.0.111", 2231 | ] 2232 | 2233 | [[package]] 2234 | name = "zerofrom" 2235 | version = "0.1.6" 2236 | source = "registry+https://github.com/rust-lang/crates.io-index" 2237 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2238 | dependencies = [ 2239 | "zerofrom-derive", 2240 | ] 2241 | 2242 | [[package]] 2243 | name = "zerofrom-derive" 2244 | version = "0.1.6" 2245 | source = "registry+https://github.com/rust-lang/crates.io-index" 2246 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2247 | dependencies = [ 2248 | "proc-macro2", 2249 | "quote", 2250 | "syn 2.0.111", 2251 | "synstructure", 2252 | ] 2253 | 2254 | [[package]] 2255 | name = "zerotrie" 2256 | version = "0.2.3" 2257 | source = "registry+https://github.com/rust-lang/crates.io-index" 2258 | checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 2259 | dependencies = [ 2260 | "displaydoc", 2261 | "yoke", 2262 | "zerofrom", 2263 | ] 2264 | 2265 | [[package]] 2266 | name = "zerovec" 2267 | version = "0.11.5" 2268 | source = "registry+https://github.com/rust-lang/crates.io-index" 2269 | checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 2270 | dependencies = [ 2271 | "yoke", 2272 | "zerofrom", 2273 | "zerovec-derive", 2274 | ] 2275 | 2276 | [[package]] 2277 | name = "zerovec-derive" 2278 | version = "0.11.2" 2279 | source = "registry+https://github.com/rust-lang/crates.io-index" 2280 | checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 2281 | dependencies = [ 2282 | "proc-macro2", 2283 | "quote", 2284 | "syn 2.0.111", 2285 | ] 2286 | --------------------------------------------------------------------------------