├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── example.md ├── examples ├── bee.jpg ├── error_handling.rs ├── inline_bot.rs ├── print_everything.rs ├── reply.rs ├── restart.rs ├── send_mediagroup.rs ├── send_memory.rs ├── send_self.rs └── unknown_cmd.rs ├── src ├── bot.rs ├── error.rs ├── file.rs ├── functions.rs ├── lib.rs └── objects.rs └── telebot-derive ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /telebot-derive/Cargo.lock 4 | /telebot-derive/target/ 5 | /target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 9 | Cargo.lock 10 | 11 | # vim swap files 12 | *~ 13 | *.swp 14 | *.swo 15 | 16 | # These are backup files generated by rustfmt 17 | **/*.rs.bk 18 | 19 | # File manager directory configuration files 20 | .directory -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - nightly 5 | 6 | sudo: false 7 | notifications: 8 | email: 9 | on_success: never 10 | 11 | script: 12 | - cargo build --verbose 13 | - 'for i in examples/*; do cargo build --verbose --example $(basename $i .rs); done' 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "telebot" 3 | edition = "2018" 4 | version = "0.3.1" 5 | 6 | authors = ["bytesnake "] 7 | license = "MIT/Apache-2.0" 8 | description = "A wrapper around the telegram bot api, powered by futures" 9 | 10 | repository = "https://github.com/bytesnake/telebot" 11 | readme = "README.md" 12 | documentation = "https://docs.rs/telebot" 13 | 14 | categories = ["network-programming"] 15 | keywords = ["telebot", "telegram", "bot", "chat", "async"] 16 | 17 | [dependencies] 18 | tokio = {version = "0.1", default-features = false, features = ["io", "reactor", "tcp"] } 19 | serde = {version = "1.0", features = ["derive"]} 20 | serde_json = "1.0" 21 | erased-serde = "0.3" 22 | futures = "0.1" 23 | hyper = "0.12" 24 | hyper-tls = "0.3.0" 25 | native-tls = "0.2" 26 | hyper-multipart-rfc7578 = "0.3" 27 | uuid = { version = "0.7", features = ["v4"] } 28 | telebot-derive = {version = "0.0.14", path = "./telebot-derive/"} 29 | log = "0.4" 30 | failure = "0.1.1" 31 | futures-retry = "0.3" 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 intermezzOS Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Telebot - Telegram Bot Library for Rust 2 | ====================================== 3 | 4 | [![Travis Build Status](https://travis-ci.org/bytesnake/telebot.svg)](https://travis-ci.org/bytesnake/telebot) 5 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bytesnake/telebot/blob/master/LICENSE) 6 | [![Crates.io](https://img.shields.io/crates/v/telebot.svg)](https://crates.io/crates/telebot) 7 | [![doc.rs](https://docs.rs/telebot/badge.svg)](https://docs.rs/telebot) 8 | 9 | This library allows you to write a Telegram Bot in the Rust language. It's an almost complete wrapper for the Telegram Bot API and uses hyper to send requests to the Telegram server. Each Telegram function call returns a future which carries the actual bot and the answer. 10 | 11 | ## Usage 12 | Add this to your `Cargo.toml` 13 | ``` toml 14 | [dependencies] 15 | telebot = "0.3.1" 16 | ``` 17 | ## How it works 18 | This example shows the basic usage of the telebot library. It creates a new handler for a simple "/reply" command and replies the received text. The tokio eventloop polls every 200ms for new updates and matches them with the registered events. If the command matches with "/reply" it will call the function and execute the returned future. 19 | 20 | ``` rust 21 | use telebot::Bot; 22 | use futures::stream::Stream; 23 | use std::env; 24 | 25 | // import all available functions 26 | use telebot::functions::*; 27 | 28 | fn main() { 29 | // Create the bot 30 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 31 | 32 | // Register a reply command which answers a message 33 | let handle = bot.new_cmd("/reply") 34 | .and_then(|(bot, msg)| { 35 | let mut text = msg.text.unwrap().clone(); 36 | if text.is_empty() { 37 | text = "".into(); 38 | } 39 | 40 | bot.message(msg.chat.id, text).send() 41 | }) 42 | .for_each(|_| Ok(())); 43 | 44 | bot.run_with(handle); 45 | } 46 | ``` 47 | 48 | ## Additional example 49 | The former example was very simple with just one handler and no error handling. If you want to see a further explained and illustrated one, please see [here](example.md). 50 | 51 | ## Find a Telegram function in the source code 52 | This crate uses custom derive to generate functions of the Telegram API. Therefore each complete function is described with a struct in [functions.rs](src/functions.rs) and the supplemental crate telebot-derive generates the complete signature. In order to find a function, the struct signature can be used. For example consider sendLocation: 53 | ``` rust 54 | /// Use this method to send point on the map. On success, the sent Message is returned. 55 | #[derive(TelegramFunction, Serialize)] 56 | #[call = "sendLocation"] 57 | #[answer = "Message"] 58 | #[function = "location"] 59 | pub struct SendLocation { 60 | chat_id: u32, 61 | latitude: f32, 62 | longitude: f32, 63 | #[serde(skip_serializing_if="Option::is_none")] 64 | disable_notification: Option, 65 | #[serde(skip_serializing_if="Option::is_none")] 66 | reply_to_message_id: Option, 67 | #[serde(skip_serializing_if="Option::is_none")] 68 | reply_markup: Option 69 | } 70 | ``` 71 | 72 | The field "function" defines the name of the function in the local API. Each optional field in the struct can be changed by calling an additional function with the name of the field. 73 | So for example to send the location of Paris to chat 432432 without notification: `bot.location(432432, 48.8566, 2.3522).disable_notification(true).send() ` 74 | 75 | ## License 76 | 77 | Licensed under either of 78 | 79 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 80 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 81 | 82 | at your option. 83 | 84 | ### Contribution 85 | 86 | Unless you explicitly state otherwise, any contribution intentionally 87 | submitted for inclusion in the work by you, as defined in the Apache-2.0 88 | license, shall be dual licensed as above, without any additional terms or 89 | conditions. 90 | -------------------------------------------------------------------------------- /example.md: -------------------------------------------------------------------------------- 1 | # Additional example 2 | In addition to the readme I will show a more sophisticated example here. We want to create a Telegram command which takes two coordinates and then send the position as a map to the user. The command looks like this: 3 | 4 | `/location 2.321 12.32` 5 | 6 | The full example can be found [here](https://github.com/bytesnake/telebot/blob/master/examples/error_handling.rs). 7 | 8 | ## What could go wrong 9 | Before we are creating the future chain, lets first think about the error enum. Either the user input is invalid (e.g. there are not two decimals after the command) or the Telegram server could have problems to process the coordinates. Therefore the enum looks like this: 10 | ``` rust 11 | enum LocationErr { 12 | Telegram(Error), 13 | WrongLocationFormat 14 | } 15 | ``` 16 | 17 | ## Create a new command 18 | We want to create a new bot and register a new command `/location`. This is very simple: 19 | ``` rust 20 | let bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()); 21 | let handle = bot.new_cmd("/location") 22 | ``` 23 | Everything else in this doc will modify the stream returned by new_cmd. 24 | 25 | ## Parse the user input 26 | After receiving a command from the user we want to parse the arguments. This can be achieved by using `split_whitespace` command to create an iterator over words and then parsing the first two elements to f32 elements. 27 | 28 | ``` rust 29 | if let Some(pos) = msg.text.next() { 30 | let mut elms = pos.split_whitespace.take(2).filter_map(|x| x.parse::().ok()); 31 | 32 | if let (Some(a), Some(l)) = (elms.next(), elms.next()) { 33 | return Ok((bot, msg, a, l)); 34 | } 35 | } 36 | 37 | return Err((bot, msg, LocationErr::WrongLocationFormat)); 38 | ``` 39 | If anything goes wrong then we will return an error. The trick is now two chain another `and_then` function in case the parsing was successful. 40 | 41 | ## Send the location 42 | Send the location is straightforward and can be accomplished with the location function. In case of an error we will wrap the Telegram error in the LocationErr::Telegram variant. 43 | ``` rust 44 | bot.location(msg.chat.id, long, alt) 45 | .send() 46 | .map_err(|err| (bot, msg, LocationErr::Telegram(err))) 47 | ``` 48 | 49 | ## Consume the error and send a message, if one occurs 50 | We can now catch any error from the previous chain with an `or_else` function. The error message is first converted to text and then send to the user. 51 | ``` rust 52 | let text = match err { 53 | LocationErr::Telegram(err) => format!("Telegram error: {:?}", err), 54 | LocationErr::WrongLocationFormat => "Couldn't parse the location!".into() 55 | }; 56 | 57 | bot.message(msg.chat.id, text).send() 58 | ``` 59 | ## Summarise 60 | If we are looking back at our approach, then it is obvious that we can create any sequence of Telegram calls. If at any point an error occurs, then we will jump to the `or_else` function. We just need enough enum variants and a long chain of `and_then`. 61 | -------------------------------------------------------------------------------- /examples/bee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesnake/telebot/d7f41872c74b4624c5329fb6df2196e03acb6a4c/examples/bee.jpg -------------------------------------------------------------------------------- /examples/error_handling.rs: -------------------------------------------------------------------------------- 1 | use telebot::{Bot, File}; 2 | use failure::Error; 3 | use futures::stream::Stream; 4 | use futures::Future; 5 | use std::env; 6 | 7 | // import all available functions 8 | use telebot::functions::*; 9 | 10 | fn main() { 11 | // Create the bot 12 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 13 | 14 | // Register a location command which will send a location to requests like /location 2.321 12.32 15 | enum LocationErr { 16 | Telegram(Error), 17 | WrongLocationFormat, 18 | } 19 | 20 | let handle = bot.new_cmd("/location") 21 | .then(|result| { 22 | let (bot, mut msg) = result.expect("Strange telegram error!"); 23 | 24 | if let Some(pos) = msg.text.take() { 25 | let mut elms = pos.split_whitespace().take(2).filter_map( 26 | |x| x.parse::().ok(), 27 | ); 28 | 29 | if let (Some(a), Some(l)) = (elms.next(), elms.next()) { 30 | return Ok((bot, msg, a, l)); 31 | } 32 | } 33 | 34 | return Err((bot, msg, LocationErr::WrongLocationFormat)); 35 | }) 36 | .and_then(|(bot, msg, long, alt)| { 37 | bot.location(msg.chat.id, long, alt).send().map_err(|err| { 38 | (bot, msg, LocationErr::Telegram(err)) 39 | }) 40 | }) 41 | .or_else(|(bot, msg, err)| { 42 | let text = { 43 | match err { 44 | LocationErr::Telegram(err) => format!("Telegram error: {:?}", err), 45 | LocationErr::WrongLocationFormat => "Couldn't parse the location!".into(), 46 | } 47 | }; 48 | 49 | bot.message(msg.chat.id, text).send() 50 | }) 51 | .for_each(|_| Ok(())); 52 | 53 | // Register a get_my_photo command which will send the own profile photo to the chat 54 | enum PhotoErr { 55 | Telegram(Error), 56 | NoPhoto, 57 | } 58 | 59 | let handle2 = bot.new_cmd("/get_my_photo") 60 | .then(|result| { 61 | let (bot, msg) = result.expect("Strange telegram error!"); 62 | 63 | let user_id = msg.from.clone().unwrap().id; 64 | 65 | bot.get_user_profile_photos(user_id) 66 | .limit(1u32) 67 | .send() 68 | .then(|result| match result { 69 | Ok((bot, photos)) => { 70 | if photos.total_count == 0 { 71 | return Err((bot, msg, PhotoErr::NoPhoto)); 72 | } 73 | 74 | return Ok((bot, msg, photos.photos[0][0].clone().file_id)); 75 | } 76 | Err(err) => Err((bot, msg, PhotoErr::Telegram(err))), 77 | }) 78 | }) 79 | .and_then(|(bot, msg, file_id)| { 80 | bot.photo(msg.chat.id) 81 | .file(File::Telegram(file_id)) 82 | .send() 83 | .map_err(|err| (bot, msg, PhotoErr::Telegram(err))) 84 | }) 85 | .or_else(|(bot, msg, err)| { 86 | let text = match err { 87 | PhotoErr::Telegram(err) => format!("Telegram Error: {:?}", err), 88 | PhotoErr::NoPhoto => "No photo exists!".into(), 89 | }; 90 | 91 | bot.message(msg.chat.id, text).send() 92 | }) 93 | .for_each(|_| Ok(())); 94 | 95 | // enter the main loop 96 | bot.run_with(handle.join(handle2)); 97 | } 98 | -------------------------------------------------------------------------------- /examples/inline_bot.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | use erased_serde::Serialize; 6 | 7 | use telebot::functions::*; 8 | use telebot::objects::*; 9 | 10 | fn main() { 11 | // Create the bot 12 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 13 | 14 | let stream = bot.inline() 15 | .and_then(|(bot, query)| { 16 | let result: Vec> = vec![ 17 | Box::new( 18 | InlineQueryResultArticle::new( 19 | "Test".into(), 20 | Box::new(input_message_content::Text::new("This is a test".into())), 21 | ).reply_markup(InlineKeyboardMarkup::new(vec![ 22 | vec![ 23 | InlineKeyboardButton::new("Wikipedia".into()) 24 | .url("http://wikipedia.org"), 25 | ], 26 | ])), 27 | ), 28 | ]; 29 | 30 | bot.answer_inline_query(query.id, result) 31 | .is_personal(true) 32 | .send() 33 | }) 34 | .for_each(|_| Ok(())); 35 | 36 | // enter the main loop 37 | bot.run_with(stream); 38 | //tokio::spawn(stream.into_future().map_err(|_| ())); 39 | 40 | //lp.run(stream.for_each(|_| Ok(())).into_future()).unwrap(); 41 | } 42 | -------------------------------------------------------------------------------- /examples/print_everything.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | use futures::{IntoFuture, Future}; 5 | 6 | fn main() { 7 | // Create the bot 8 | let bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 9 | 10 | let stream = bot.get_stream(None).for_each(|(_, msg)| { 11 | println!("Received: {:#?}", msg); 12 | 13 | Ok(()) 14 | }); 15 | 16 | // enter the main loop 17 | tokio::run(stream.into_future().map_err(|_| ())); 18 | /*let res = lp.run(stream.for_each(|_| Ok(())).into_future()); 19 | if let Err(err) = res { 20 | eprintln!("Event loop shutdown:"); 21 | for (i, cause) in err.iter_causes().enumerate() { 22 | eprintln!(" => {}: {}", i, cause); 23 | } 24 | }*/ 25 | } 26 | -------------------------------------------------------------------------------- /examples/reply.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | // Register a reply command which answers a message 13 | let handle = bot.new_cmd("/reply") 14 | .and_then(|(bot, msg)| { 15 | let mut text = msg.text.unwrap().clone(); 16 | if text.is_empty() { 17 | text = "".into(); 18 | } 19 | 20 | bot.message(msg.chat.id, text).send() 21 | }) 22 | .for_each(|_| Ok(())); 23 | 24 | bot.run_with(handle); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/restart.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | // Enter the main loop 13 | loop { 14 | bot.clone().run(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/send_mediagroup.rs: -------------------------------------------------------------------------------- 1 | use telebot::{Bot, file::File}; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | let handle = bot.new_cmd("/send_mediagroup") 13 | .and_then(|(bot, msg)| { 14 | bot.mediagroup(msg.chat.id) 15 | .file(File::Url("https://upload.wikimedia.org/wikipedia/commons/f/f4/Honeycrisp.jpg".into())) 16 | .file(File::Url("https://upload.wikimedia.org/wikipedia/en/3/3e/Pooh_Shepard1928.jpg".into())) 17 | .file("examples/bee.jpg") 18 | .send() 19 | }) 20 | .for_each(|_| Ok(())); 21 | 22 | // enter the main loop 23 | bot.run_with(handle); 24 | } 25 | -------------------------------------------------------------------------------- /examples/send_memory.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | let text = r" 13 | Dearest creature in creation, 14 | Study English pronunciation. 15 | I will teach you in my verse 16 | Sounds like corpse, corps, horse, and worse. 17 | I will keep you, Suzy, busy, 18 | Make your head with heat grow dizzy. 19 | Tear in eye, your dress will tear. 20 | So shall I! Oh hear my prayer. 21 | 22 | Just compare heart, beard, and heard, 23 | Dies and diet, lord and word, 24 | Sword and sward, retain and Britain. 25 | (Mind the latter, how it's written.) 26 | Now I surely will not plague you 27 | With such words as plaque and ague. 28 | But be careful how you speak: 29 | Say break and steak, but bleak and streak; 30 | Cloven, oven, how and low, 31 | Script, receipt, show, poem, and toe. 32 | 33 | Hear me say, devoid of trickery, 34 | Daughter, laughter, and Terpsichore, 35 | Typhoid, measles, topsails, aisles, 36 | Exiles, similes, and reviles; 37 | Scholar, vicar, and cigar, 38 | Solar, mica, war and far; 39 | One, anemone, Balmoral, 40 | Kitchen, lichen, laundry, laurel; 41 | Gertrude, German, wind and mind, 42 | Scene, Melpomene, mankind. 43 | 44 | ..."; 45 | 46 | let handle = bot.new_cmd("/send") 47 | .and_then(move |(bot, msg)| { 48 | bot.document(msg.chat.id) 49 | .file(("poem.txt", text.as_bytes())) 50 | .caption("The Chaos") 51 | .send() 52 | }) 53 | .for_each(|_| Ok(())); 54 | 55 | // enter the main loop 56 | bot.run_with(handle); 57 | } 58 | -------------------------------------------------------------------------------- /examples/send_self.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::stream::Stream; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | let handle = bot.new_cmd("/send_self") 13 | .and_then(|(bot, msg)| { 14 | bot.document(msg.chat.id) 15 | .file("examples/send_self.rs") 16 | .send() 17 | }) 18 | .for_each(|_| Ok(())); 19 | 20 | // enter the main loop 21 | bot.run_with(handle); 22 | } 23 | -------------------------------------------------------------------------------- /examples/unknown_cmd.rs: -------------------------------------------------------------------------------- 1 | use telebot::Bot; 2 | use futures::{Future, stream::Stream}; 3 | use std::env; 4 | 5 | // import all available functions 6 | use telebot::functions::*; 7 | 8 | fn main() { 9 | // Create the bot 10 | let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 11 | 12 | let known = bot.new_cmd("/known") 13 | .and_then(|(bot, msg)| bot.message(msg.chat.id, "This one is known".into()).send()) 14 | .for_each(|_| Ok(())); 15 | 16 | // Every possible command is unknown 17 | let unknown = bot.unknown_cmd() 18 | .and_then(|(bot, msg)| bot.message(msg.chat.id, "Unknown command".into()).send()) 19 | .for_each(|_| Ok(())); 20 | 21 | // Enter the main loop 22 | bot.run_with(known.join(unknown)); 23 | } 24 | -------------------------------------------------------------------------------- /src/bot.rs: -------------------------------------------------------------------------------- 1 | //! The bot module 2 | 3 | use crate::objects; 4 | //use functions::FunctionGetMe; 5 | use crate::error::{ErrorKind, TelegramError}; 6 | use crate::file::File; 7 | 8 | use std::{str, time::{Duration, Instant}, collections::HashMap, sync::Arc}; 9 | use std::sync::atomic::{AtomicUsize, Ordering}; 10 | 11 | use tokio::timer::Interval; 12 | use hyper::{Body as Body2, Client, Request, Uri, header::CONTENT_TYPE, client::{HttpConnector, ResponseFuture}, rt::Stream}; 13 | use hyper_tls::HttpsConnector; 14 | use hyper_multipart::client::multipart; 15 | use serde_json::{self, value::Value}; 16 | use futures::{stream, Future, future::IntoFuture, sync::mpsc::{self, UnboundedSender}}; 17 | use failure::{Error, Fail, ResultExt}; 18 | use hyper_multipart_rfc7578::client::multipart::Body; 19 | 20 | /// A clonable request handle struct 21 | /// Allows the construction of requests to the Telegram server 22 | #[derive(Clone)] 23 | pub struct RequestHandle { 24 | key: String, 25 | pub inner: Arc, Body2>> 26 | } 27 | 28 | impl RequestHandle { 29 | /// Creates a new request and adds a JSON message to it. The returned Future contains a the 30 | /// reply as a string. This method should be used if no file is added becontext a JSON msg is 31 | /// always compacter than a formdata one. 32 | pub fn fetch_json( 33 | &self, 34 | func: &'static str, 35 | msg: &str, 36 | ) -> impl Future { 37 | 38 | debug!("Send JSON {}: {}", func, msg); 39 | let request = self.build_json(func, String::from(msg)).unwrap(); 40 | 41 | _fetch(self.inner.request(request)) 42 | } 43 | 44 | /// Builds the HTTP header for a JSON request. The JSON is already converted to a str and is 45 | /// appended to the POST header. 46 | fn build_json( 47 | &self, 48 | func: &'static str, 49 | msg: String, 50 | ) -> Result, Error> { 51 | let url: Result = 52 | format!("https://api.telegram.org/bot{}/{}", self.key, func).parse(); 53 | 54 | debug!("Send message {}", msg); 55 | 56 | let req = Request::post(url.context(ErrorKind::Uri)?) 57 | .header(CONTENT_TYPE, "application/json") 58 | .body(msg.into()) 59 | .context(ErrorKind::Hyper)?; 60 | 61 | Ok(req) 62 | } 63 | /// 64 | /// Creates a new request with some byte content (e.g. a file). The method properties have to be 65 | /// in the formdata setup and cannot be sent as JSON. 66 | pub fn fetch_formdata( 67 | &self, 68 | func: &'static str, 69 | msg: &Value, 70 | files: Vec, 71 | kind: &str, 72 | ) -> impl Future { 73 | debug!("Send formdata {}: {}", func, msg.to_string()); 74 | 75 | let request = self.build_formdata(func, msg, files, kind).unwrap(); 76 | _fetch(self.inner.request(request)) 77 | } 78 | 79 | /// Builds the HTTP header for a formdata request. The file content is read and then append to 80 | /// the formdata. Each key-value pair has a own line. 81 | fn build_formdata( 82 | &self, 83 | func: &'static str, 84 | msg: &Value, 85 | files: Vec, 86 | _kind: &str, 87 | ) -> Result,Error> { 88 | let url: Result = 89 | format!("https://api.telegram.org/bot{}/{}", self.key, func).parse(); 90 | 91 | let mut req_builder = Request::post(url.context(ErrorKind::Uri)?); 92 | let mut form = multipart::Form::default(); 93 | 94 | let msg = msg.as_object().ok_or(ErrorKind::JsonNotMap)?; 95 | 96 | // add properties 97 | for (key, val) in msg.iter() { 98 | let val = match val { 99 | &Value::String(ref val) => format!("{}", val), 100 | etc => format!("{}", etc), 101 | }; 102 | 103 | form.add_text(key, val); 104 | } 105 | 106 | for file in files { 107 | match file { 108 | File::Memory { name, source } => { 109 | form.add_reader_file(name.clone(), source, name); 110 | } 111 | File::Disk { path } => { 112 | form.add_file(path.clone().file_name().unwrap().to_str().unwrap(), path).context(ErrorKind::NoFile)?; 113 | }, 114 | _ => {} 115 | } 116 | } 117 | 118 | let req = form.set_body_convert::(&mut req_builder).context(ErrorKind::Hyper)?; 119 | 120 | Ok(req) 121 | } 122 | } 123 | /// Calls the Telegram API for the function and awaits the result. The result is then converted 124 | /// to a String and returned in a Future. 125 | //pub fn _fetch>(fut_res: FutureRetry ResponseFuture, T>) -> impl Future { 126 | //pub fn _fetch, Error=hyper::error::Error>>(fut_res: T) -> impl Future { 127 | pub fn _fetch(fut_res: ResponseFuture) -> impl Future { 128 | fut_res 129 | .and_then(move |res| res.into_body().concat2()) 130 | .map_err(|e| { 131 | eprintln!("{:?}", e); 132 | 133 | Error::from(e.context(ErrorKind::Hyper)) 134 | }) 135 | .and_then(move |response_chunks| { 136 | let s = str::from_utf8(&response_chunks)?; 137 | 138 | debug!("Got a result from telegram: {}", s); 139 | // try to parse the result as a JSON and find the OK field. 140 | // If the ok field is true, then the string in "result" will be returned 141 | let req = serde_json::from_str::(&s).context(ErrorKind::JsonParse)?; 142 | 143 | let ok = req.get("ok") 144 | .and_then(Value::as_bool) 145 | .ok_or(ErrorKind::Json)?; 146 | 147 | if ok { 148 | if let Some(result) = req.get("result") { 149 | return Ok(serde_json::to_string(result).context(ErrorKind::JsonSerialize)?); 150 | } 151 | } 152 | 153 | let e = match req.get("description").and_then(Value::as_str) { 154 | Some(err) => { 155 | Error::from(TelegramError::new(err.into()).context(ErrorKind::Telegram)) 156 | } 157 | None => Error::from(ErrorKind::Telegram), 158 | }; 159 | 160 | Err(Error::from(e.context(ErrorKind::Telegram))) 161 | }) 162 | } 163 | 164 | /// The main bot structure 165 | /// 166 | /// Contains all configuration like `key`, `name`, etc. important handles to message the user and 167 | /// `request` to issue requests to the Telegram server 168 | #[derive(Clone)] 169 | pub struct Bot { 170 | pub request: RequestHandle, 171 | key: String, 172 | name: Option, 173 | update_interval: u64, 174 | timeout: u64, 175 | pub handlers: HashMap>, 176 | pub unknown_handler: Option>, 177 | pub callback_handler: Option>, 178 | pub inline_handler: Option> 179 | } 180 | 181 | impl Bot { 182 | pub fn new(key: &str) -> Bot { 183 | let client = Client::builder() 184 | .keep_alive(true) 185 | .keep_alive_timeout(Some(Duration::from_secs(3600))) 186 | .build(HttpsConnector::new(4).unwrap()); 187 | 188 | Bot { 189 | request: RequestHandle { inner: Arc::new(client), key: key.into() }, 190 | key: key.into(), 191 | name: None, 192 | update_interval: 2000, 193 | timeout: 3600, 194 | handlers: HashMap::new(), 195 | unknown_handler: None, 196 | callback_handler: None, 197 | inline_handler: None 198 | } 199 | } 200 | 201 | /// Sets the update interval to an integer in milliseconds 202 | pub fn update_interval(mut self, interval: u64) -> Bot { 203 | self.update_interval = interval; 204 | 205 | self 206 | } 207 | 208 | /// Sets the timeout interval for long polling 209 | pub fn timeout(mut self, timeout: u64) -> Bot { 210 | self.timeout = timeout; 211 | 212 | let client = Client::builder() 213 | .keep_alive(true) 214 | .keep_alive_timeout(Some(Duration::from_secs(timeout))) 215 | .build(HttpsConnector::new(4).unwrap()); 216 | 217 | self.request = RequestHandle { inner: Arc::new(client), key: self.key.clone() }; 218 | 219 | self 220 | } 221 | 222 | /// Creates a new command and returns a stream which will yield a message when the command is send 223 | pub fn new_cmd( 224 | &mut self, 225 | cmd: &str, 226 | ) -> impl Stream { 227 | let (sender, receiver) = mpsc::unbounded(); 228 | 229 | let cmd = if cmd.starts_with("/") { 230 | cmd.into() 231 | } else { 232 | format!("/{}", cmd) 233 | }; 234 | 235 | self.handlers.insert(cmd.into(), sender); 236 | 237 | receiver.map_err(|_| Error::from(ErrorKind::Channel)) 238 | } 239 | 240 | /// Returns a stream which will yield a message when none of previously registered commands matches 241 | pub fn unknown_cmd(&mut self) -> impl Stream { 242 | let (sender, receiver) = mpsc::unbounded(); 243 | 244 | self.unknown_handler = Some(sender); 245 | 246 | receiver.then(|x| x.map_err(|_| Error::from(ErrorKind::Channel))) 247 | } 248 | 249 | /// Returns a stream which will yield a received CallbackQuery 250 | pub fn callback(&mut self) -> impl Stream { 251 | let (sender, receiver) = mpsc::unbounded(); 252 | 253 | self.callback_handler = Some(sender); 254 | 255 | receiver.then(|x| x.map_err(|_| Error::from(ErrorKind::Channel))) 256 | } 257 | 258 | /// Returns a stream which will yield a received CallbackQuery 259 | pub fn inline(&mut self) -> impl Stream { 260 | let (sender, receiver) = mpsc::unbounded(); 261 | 262 | self.inline_handler = Some(sender); 263 | 264 | receiver.then(|x| x.map_err(|_| Error::from(ErrorKind::Channel))) 265 | } 266 | 267 | pub fn resolve_name(&self) -> impl Future, Error = Error> { 268 | use crate::functions::FunctionGetMe; 269 | 270 | // create a new task which resolves the bot name and then set it in the struct 271 | let resolve_name = self.request.get_me().send() 272 | .map(move |user| { 273 | if let Some(name) = user.1.username { 274 | return Some(format!("@{}", name)); 275 | } else { 276 | return None; 277 | } 278 | }); 279 | 280 | resolve_name 281 | } 282 | 283 | pub fn process_updates(self, last_id: Arc) -> impl Stream { 284 | use crate::functions::FunctionGetUpdates; 285 | 286 | self.request.get_updates() 287 | .offset(last_id.load(Ordering::Relaxed) as i64) 288 | .timeout(self.timeout as i64) 289 | .send() 290 | .into_stream() 291 | .map(|(_, x)| { 292 | stream::iter_result( 293 | x.0 294 | .into_iter() 295 | .map(|x| Ok(x)) 296 | .collect::>>(), 297 | ) 298 | }) 299 | .flatten() 300 | .and_then(move |x| { 301 | if last_id.load(Ordering::Relaxed) < x.update_id as usize + 1 { 302 | last_id.store(x.update_id as usize + 1, Ordering::Relaxed); 303 | } 304 | 305 | Ok(x) 306 | }) 307 | .filter_map(move |mut val| { 308 | debug!("Got an update from Telegram: {:?}", val); 309 | 310 | if let Some(_) = val.callback_query { 311 | if let Some(sender) = self.callback_handler.clone() { 312 | sender 313 | .unbounded_send((self.request.clone(), val.callback_query.unwrap())) 314 | .unwrap_or_else(|e| error!("Error: {}", e)); 315 | return None; 316 | } 317 | } 318 | 319 | if let Some(_) = val.inline_query { 320 | if let Some(sender) = self.inline_handler.clone() { 321 | sender 322 | .unbounded_send((self.request.clone(), val.inline_query.unwrap())) 323 | .unwrap_or_else(|e| error!("Error: {}", e)); 324 | return None; 325 | } 326 | } 327 | 328 | let mut sndr: Option> = None; 329 | 330 | if let Some(ref mut message) = val.message { 331 | if let Some(true) = message.entities.as_ref().and_then(|x| x.get(0)).map(|x| x.kind == "bot_command") { 332 | if let Some(text) = message.text.clone() { 333 | let mut content = text.split_whitespace(); 334 | if let Some(mut cmd) = content.next() { 335 | if let Some(name) = self.name.as_ref() { 336 | if cmd.ends_with(name.as_str()) { 337 | cmd = cmd.rsplitn(2, '@').skip(1).next().unwrap(); 338 | } 339 | } 340 | if let Some(sender) = self.handlers.get(cmd) 341 | { 342 | sndr = Some(sender.clone()); 343 | message.text = Some(content.collect::>().join(" ")); 344 | } else if let Some(ref sender) = 345 | self.unknown_handler 346 | { 347 | sndr = Some(sender.clone()); 348 | } 349 | } 350 | } 351 | } 352 | } 353 | 354 | if let Some(sender) = sndr { 355 | sender 356 | .unbounded_send((self.request.clone(), val.message.unwrap())) 357 | .unwrap_or_else(|e| error!("Error: {}", e)); 358 | return None; 359 | } else { 360 | return Some((self.request.clone(), val)); 361 | } 362 | }) 363 | } 364 | 365 | /// 366 | /// The main update loop, the update function is called every update_interval milliseconds 367 | /// When an update is available the last_id will be updated and the message is filtered 368 | /// for commands 369 | /// The message is forwarded to the returned stream if no command was found 370 | pub fn get_stream( 371 | mut self, 372 | name: Option 373 | ) -> impl Stream { 374 | self.name = name; 375 | let last_id = Arc::new(AtomicUsize::new(0)); 376 | 377 | let duration = Duration::from_millis(self.update_interval); 378 | Interval::new(Instant::now(), duration) 379 | .map_err(|x| Error::from(x.context(ErrorKind::IntervalTimer))) 380 | .map(move |_| self.clone().process_updates(last_id.clone())) 381 | .flatten() 382 | /*.inspect_err(|err| println!("Error on {:?}", err)) 383 | .then(|r| futures::future::ok(stream::iter_ok::<_, Error>(r))) 384 | .flatten()*/ 385 | } 386 | 387 | pub fn into_future(&self) -> impl Future { 388 | let bot = self.clone(); 389 | 390 | self.resolve_name() 391 | .and_then(|name| bot.get_stream(name).for_each(|_| Ok(()))) 392 | .map(|_| ()) 393 | } 394 | 395 | pub fn run_with(self, other: I) 396 | where 397 | I: IntoFuture, 398 | ::Future: Send + 'static, 399 | ::Item: Send 400 | { 401 | tokio::run( 402 | self.into_future().join(other) 403 | .map(|_| ()) 404 | .map_err(|e| { 405 | eprintln!("Error: could not resolve the bot name!"); 406 | 407 | for (i, cause) in e.iter_causes().enumerate() { 408 | println!(" => {}: {}", i, cause); 409 | } 410 | }) 411 | ); 412 | } 413 | 414 | pub fn run(self) { 415 | self.run_with(Ok(())); 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use failure::{Backtrace, Context, Fail}; 4 | 5 | #[derive(Debug)] 6 | pub struct Error { 7 | inner: Context, 8 | } 9 | 10 | impl Fail for Error { 11 | fn cause(&self) -> Option<&Fail> { 12 | self.inner.cause() 13 | } 14 | 15 | fn backtrace(&self) -> Option<&Backtrace> { 16 | self.inner.backtrace() 17 | } 18 | } 19 | 20 | impl fmt::Display for Error { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | fmt::Display::fmt(&self.inner, f) 23 | } 24 | } 25 | 26 | impl Error { 27 | pub fn kind(&self) -> ErrorKind { 28 | *self.inner.get_context() 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(kind: ErrorKind) -> Error { 34 | Error { 35 | inner: Context::new(kind), 36 | } 37 | } 38 | } 39 | 40 | impl From> for Error { 41 | fn from(inner: Context) -> Error { 42 | Error { inner: inner } 43 | } 44 | } 45 | 46 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] 47 | pub enum ErrorKind { 48 | // indicates that the received reply couldn't be decoded (e.g. caused by an aborted 49 | // connection) 50 | #[fail(display = "Wrong string format, couldn't parse as UTF8")] 51 | UTF8Decode, 52 | 53 | // indicates a Telegram error (e.g. a property is missing) 54 | #[fail(display = "Telegram server responded with an error")] 55 | Telegram, 56 | 57 | #[fail(display = "Failed to read file for upload")] 58 | TelegramFileRead, 59 | 60 | #[fail(display = "There was an error initializing HTTPS")] 61 | HttpsInitializeError, 62 | 63 | // indicates some failure in Hyper, missing network connection, etc. 64 | #[fail(display = "There was an error fetching the content")] 65 | Hyper, 66 | 67 | // indicates some failure with parsing a URI 68 | #[fail(display = "There was an error parsing the URI")] 69 | Uri, 70 | 71 | // indicates an error reading or writing data 72 | #[fail(display = "Failed to read or write data")] 73 | IO, 74 | 75 | // indicates a malformated reply, this should never happen unless the Telegram server has a 76 | // hard time 77 | #[fail(display = "Failed to parse a JSON string")] 78 | JsonParse, 79 | 80 | #[fail(display = "Failed to serialize to JSON")] 81 | JsonSerialize, 82 | 83 | #[fail(display = "Json from server is malformed")] 84 | Json, 85 | 86 | #[fail(display = "Failed to create a channel")] 87 | Channel, 88 | 89 | #[fail(display = "Failed to create the interval timer")] 90 | IntervalTimer, 91 | 92 | #[fail(display = "Tokio library caused an error")] 93 | Tokio, 94 | 95 | #[fail(display = "Please specify a file")] 96 | NoFile, 97 | 98 | #[fail(display = "Expected JSON to be a Map, got something else")] 99 | JsonNotMap, 100 | 101 | // indicates an unknown error 102 | #[fail(display = "Unknown error")] 103 | Unknown, 104 | } 105 | 106 | #[derive(Debug, Fail)] 107 | #[fail(display = "{}", message)] 108 | pub struct TelegramError { 109 | message: String, 110 | } 111 | 112 | impl TelegramError { 113 | pub fn new(message: String) -> Self { 114 | TelegramError { message } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | //! A Telegram file which contains a readable source and a filename 2 | //! 3 | //! The filename should be such that it represents the content type. 4 | 5 | use std::{io::Read, path::PathBuf}; 6 | use failure::Error; 7 | use crate::error::ErrorKind; 8 | 9 | #[derive(Serialize)] 10 | #[serde(untagged)] 11 | pub enum MediaFile { 12 | SingleFile(String), 13 | MultipleFiles(Vec) 14 | } 15 | 16 | #[derive(Serialize)] 17 | #[serde(untagged)] 18 | pub enum FileEntity { 19 | Photo { 20 | #[serde(rename = "type")] 21 | type_: &'static str, 22 | media: String, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | caption: Option, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | parse_mode: Option, 27 | }, 28 | Video {} 29 | } 30 | 31 | pub struct FileList(pub Vec); 32 | 33 | impl FileList { 34 | pub fn to_metadata(&self) -> Option { 35 | if self.0.len() == 0 { 36 | None 37 | } else if self.0.len() == 1 { 38 | Some(MediaFile::SingleFile(self.0.iter().map(|x| x.file.name()).next().unwrap())) 39 | } else { 40 | let entities = self.0.iter().map(|x| { 41 | FileEntity::Photo { 42 | type_: "photo", 43 | media: x.file.name(), 44 | caption: x.caption.clone(), 45 | parse_mode: x.parse_mode.clone() 46 | } 47 | }).collect(); 48 | 49 | Some(MediaFile::MultipleFiles(entities)) 50 | } 51 | } 52 | 53 | pub fn into_files(self) -> Option> { 54 | if self.0.len() == 0 { 55 | None 56 | } else { 57 | Some(self.0.into_iter().map(|x| x.file).collect()) 58 | } 59 | } 60 | 61 | pub fn push(&mut self, val: FileWithCaption) { 62 | self.0.push(val); 63 | } 64 | } 65 | 66 | //trait ReadAndClone: Clone + Read {} 67 | //impl ReadAndClone for T where T: Clone + Read{} 68 | 69 | /// A Telegram file which contains a readable source and a filename 70 | pub enum File { 71 | Memory { 72 | name: String, 73 | source: Box, 74 | }, 75 | Disk { 76 | path: PathBuf 77 | }, 78 | Telegram(String), 79 | Url(String) 80 | } 81 | 82 | impl File { 83 | pub fn name(&self) -> String { 84 | match self { 85 | File::Memory { name, .. } => format!("attach://{}", name), 86 | File::Disk { path, .. } => format!("attach://{}", path.file_name().unwrap().to_str().unwrap()), 87 | File::Telegram(id) => id.clone(), 88 | File::Url(url) => url.clone() 89 | } 90 | } 91 | 92 | pub fn try_from(value: T) -> Result { 93 | value.try_into() 94 | } 95 | } 96 | 97 | pub struct FileWithCaption { 98 | file: File, 99 | caption: Option, 100 | parse_mode: Option 101 | } 102 | 103 | impl FileWithCaption { 104 | pub fn new_empty(file: File) -> FileWithCaption { 105 | FileWithCaption { 106 | file: file, 107 | caption: None, 108 | parse_mode: None 109 | } 110 | } 111 | 112 | pub fn new(file: File, caption: String, parse_mode: String) -> FileWithCaption { 113 | FileWithCaption { 114 | file: file, 115 | caption: Some(caption), 116 | parse_mode: Some(parse_mode) 117 | } 118 | } 119 | } 120 | 121 | pub trait TryIntoFile: Sized { 122 | type Error; 123 | fn try_into(self) -> Result; 124 | } 125 | 126 | impl TryIntoFile for File { 127 | type Error = (); 128 | 129 | fn try_into(self) -> Result { 130 | Ok(self) 131 | } 132 | } 133 | 134 | /// Construct a Telegram file from a local path 135 | impl<'a> TryIntoFile for &'a str { 136 | type Error = Error; 137 | 138 | fn try_into(self) -> Result { 139 | let mut file = PathBuf::new(); 140 | 141 | file.push(self); 142 | 143 | if file.is_file() { 144 | Ok(File::Disk { path: file }) 145 | } else { 146 | Err(Error::from(ErrorKind::NoFile)) 147 | } 148 | } 149 | } 150 | 151 | /// Construct a Telegram file from an object which implements the Read trait 152 | impl<'a, S: Read + Send + 'static> TryIntoFile for (&'a str, S) { 153 | type Error = Error; 154 | 155 | fn try_into(self) -> Result 156 | where 157 | S: Read 158 | { 159 | let (name, source) = self; 160 | Ok(File::Memory { 161 | name: name.into(), 162 | source: Box::new(source), 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/functions.rs: -------------------------------------------------------------------------------- 1 | //! Available telegram functions, copied from https://core.telegram.org/bots/api#available-methods 2 | //! 3 | //! telebot-derive implements setter, setter and send methods to each struct 4 | 5 | use std::convert::From; 6 | 7 | use serde_json; 8 | use failure::{Error, Fail}; 9 | use futures::Future; 10 | use erased_serde::Serialize; 11 | 12 | use crate::bot::RequestHandle; 13 | use crate::objects::{self, Integer}; 14 | use crate::file::{self, MediaFile}; 15 | use crate::error::ErrorKind; 16 | 17 | /// The strongly typed version of the parse_mode field which indicates the type of text 18 | #[derive(Serialize)] 19 | pub enum ParseMode { 20 | Markdown, 21 | HTML, 22 | Text, 23 | } 24 | 25 | impl Into for ParseMode { 26 | fn into(self) -> String { 27 | let tmp = match self { 28 | ParseMode::Markdown => "Markdown", 29 | ParseMode::HTML => "HTML", 30 | ParseMode::Text => "Text", 31 | }; 32 | 33 | tmp.into() 34 | } 35 | } 36 | 37 | /// The strongly typed version of the action field which indicates the type of action 38 | pub enum Action { 39 | Typing, 40 | UploadPhoto, 41 | RecordVideo, 42 | UploadVideo, 43 | RecordAudio, 44 | UploadAudio, 45 | UploadDocument, 46 | FindLocation, 47 | } 48 | 49 | /// Possible types of reply markups 50 | pub enum ReplyMarkup { 51 | InlineKeyboardMarkup(objects::InlineKeyboardMarkup), 52 | ReplyKeyboardMarkup(objects::ReplyKeyboardMarkup), 53 | ReplyKeyboardRemove(objects::ReplyKeyboardRemove), 54 | ForceReply(objects::ForceReply), 55 | } 56 | 57 | impl ::serde::Serialize for ReplyMarkup { 58 | fn serialize(&self, serializer: S) -> Result 59 | where 60 | S: ::serde::Serializer, 61 | { 62 | use self::ReplyMarkup::*; 63 | 64 | match self { 65 | &InlineKeyboardMarkup(ref x) => x.serialize(serializer), 66 | &ReplyKeyboardMarkup(ref x) => x.serialize(serializer), 67 | &ReplyKeyboardRemove(ref x) => x.serialize(serializer), 68 | &ForceReply(ref x) => x.serialize(serializer), 69 | } 70 | } 71 | } 72 | 73 | impl From for ReplyMarkup { 74 | fn from(f: objects::InlineKeyboardMarkup) -> Self { 75 | ReplyMarkup::InlineKeyboardMarkup(f) 76 | } 77 | } 78 | 79 | impl From for ReplyMarkup { 80 | fn from(f: objects::ReplyKeyboardMarkup) -> Self { 81 | ReplyMarkup::ReplyKeyboardMarkup(f) 82 | } 83 | } 84 | 85 | impl From for ReplyMarkup { 86 | fn from(f: objects::ReplyKeyboardRemove) -> Self { 87 | ReplyMarkup::ReplyKeyboardRemove(f) 88 | } 89 | } 90 | 91 | impl From for ReplyMarkup { 92 | fn from(f: objects::ForceReply) -> Self { 93 | ReplyMarkup::ForceReply(f) 94 | } 95 | } 96 | 97 | impl Into for Action { 98 | fn into(self) -> String { 99 | let tmp = match self { 100 | Action::Typing => "Typing", 101 | Action::UploadPhoto => "UploadPhoto", 102 | Action::RecordVideo => "RecordVideo", 103 | Action::UploadVideo => "UploadVideo", 104 | Action::RecordAudio => "RecordVideo", 105 | Action::UploadAudio => "UploadAudio", 106 | Action::UploadDocument => "UploadDocument", 107 | Action::FindLocation => "FindLocation", 108 | }; 109 | 110 | tmp.into() 111 | } 112 | } 113 | 114 | /// A simple method for testing your bot's auth token. Requires no parameters. Returns basic 115 | /// information about the bot in form of a User object. 116 | #[derive(TelegramFunction, Serialize)] 117 | #[call = "getMe"] 118 | #[answer = "User"] 119 | #[function = "get_me"] 120 | pub struct GetMe; 121 | 122 | /// Use this method to receive incoming updates using long polling (wiki). An Array of Update 123 | /// objects is returned. 124 | #[derive(TelegramFunction, Serialize)] 125 | #[call = "getUpdates"] 126 | #[answer = "Updates"] 127 | #[function = "get_updates"] 128 | pub struct GetUpdates { 129 | #[serde(skip_serializing_if = "Option::is_none")] 130 | offset: Option, 131 | #[serde(skip_serializing_if = "Option::is_none")] 132 | limit: Option, 133 | #[serde(skip_serializing_if = "Option::is_none")] 134 | timeout: Option, 135 | #[serde(skip_serializing_if = "Option::is_none")] 136 | allowed_updates: Option>, 137 | } 138 | 139 | /// Use this method to send text messages. On success, the sent Message is returned. 140 | #[derive(TelegramFunction, Serialize)] 141 | #[call = "sendMessage"] 142 | #[answer = "Message"] 143 | #[function = "message"] 144 | pub struct SendMessage { 145 | chat_id: Integer, 146 | text: String, 147 | #[serde(skip_serializing_if = "Option::is_none")] 148 | parse_mode: Option, 149 | #[serde(skip_serializing_if = "Option::is_none")] 150 | disable_web_page_preview: Option, 151 | #[serde(skip_serializing_if = "Option::is_none")] 152 | disable_notification: Option, 153 | #[serde(skip_serializing_if = "Option::is_none")] 154 | reply_to_message_id: Option, 155 | #[serde(skip_serializing_if = "Option::is_none")] 156 | reply_markup: Option, 157 | } 158 | 159 | /// Use this method to forward messages. On success, the sent Message is returned. 160 | #[derive(TelegramFunction, Serialize)] 161 | #[call = "forwardMessage"] 162 | #[answer = "Message"] 163 | #[function = "forward"] 164 | pub struct ForwardMessage { 165 | chat_id: Integer, 166 | from_chat_id: Integer, 167 | message_id: Integer, 168 | #[serde(skip_serializing_if = "Option::is_none")] 169 | disable_notification: Option, 170 | } 171 | 172 | /// Use this method to send photos. On success, the sent Message is returned. 173 | #[derive(TelegramFunction, Serialize)] 174 | #[call = "sendPhoto"] 175 | #[answer = "Message"] 176 | #[function = "photo"] 177 | #[file_kind = "photo"] 178 | pub struct SendPhoto { 179 | chat_id: Integer, 180 | #[serde(skip_serializing_if = "Option::is_none")] 181 | photo: Option, 182 | #[serde(skip_serializing_if = "Option::is_none")] 183 | caption: Option, 184 | #[serde(skip_serializing_if = "Option::is_none")] 185 | parse_mode: Option, 186 | #[serde(skip_serializing_if = "Option::is_none")] 187 | disable_notification: Option, 188 | #[serde(skip_serializing_if = "Option::is_none")] 189 | reply_to_message_id: Option, 190 | #[serde(skip_serializing_if = "Option::is_none")] 191 | reply_markup: Option, 192 | } 193 | 194 | /// Use this method to send audio files, if you want Telegram clients to display them in the music 195 | /// player. Your audio must be in the .mp3 format. On success, the sent Message is returned. Bots 196 | /// can currently send audio files of up to 50 MB in size, this limit may be changed in the future. 197 | /// 198 | /// For sending voice messages, use the sendVoice method instead. 199 | #[derive(TelegramFunction, Serialize)] 200 | #[call = "sendAudio"] 201 | #[answer = "Message"] 202 | #[function = "audio"] 203 | #[file_kind = "audio"] 204 | pub struct SendAudio { 205 | chat_id: Integer, 206 | #[serde(skip_serializing_if = "Option::is_none")] 207 | audio: Option, 208 | #[serde(skip_serializing_if = "Option::is_none")] 209 | caption: Option, 210 | #[serde(skip_serializing_if = "Option::is_none")] 211 | parse_mode: Option, 212 | #[serde(skip_serializing_if = "Option::is_none")] 213 | duration: Option, 214 | #[serde(skip_serializing_if = "Option::is_none")] 215 | performer: Option, 216 | #[serde(skip_serializing_if = "Option::is_none")] 217 | title: Option, 218 | #[serde(skip_serializing_if = "Option::is_none")] 219 | disable_notification: Option, 220 | #[serde(skip_serializing_if = "Option::is_none")] 221 | reply_to_message_id: Option, 222 | #[serde(skip_serializing_if = "Option::is_none")] 223 | reply_markup: Option, 224 | } 225 | 226 | /// Use this method to send general files. On success, the sent Message is returned. Bots can 227 | /// currently send files of any type of up to 50 MB in size, this limit may be changed in the 228 | /// future. 229 | #[derive(TelegramFunction, Serialize)] 230 | #[call = "sendDocument"] 231 | #[answer = "Message"] 232 | #[function = "document"] 233 | #[file_kind = "document"] 234 | pub struct SendDocument { 235 | chat_id: Integer, 236 | #[serde(skip_serializing_if = "Option::is_none")] 237 | document: Option, 238 | #[serde(skip_serializing_if = "Option::is_none")] 239 | caption: Option, 240 | #[serde(skip_serializing_if = "Option::is_none")] 241 | parse_mode: Option, 242 | #[serde(skip_serializing_if = "Option::is_none")] 243 | disable_notification: Option, 244 | #[serde(skip_serializing_if = "Option::is_none")] 245 | reply_to_message_id: Option, 246 | #[serde(skip_serializing_if = "Option::is_none")] 247 | reply_markup: Option, 248 | } 249 | 250 | /// Use this method to send .webp stickers. On success, the sent Message is returned. 251 | #[derive(TelegramFunction, Serialize)] 252 | #[call = "sendSticker"] 253 | #[answer = "Message"] 254 | #[function = "sticker"] 255 | #[file_kind = "sticker"] 256 | pub struct SendSticker { 257 | chat_id: Integer, 258 | #[serde(skip_serializing_if = "Option::is_none")] 259 | sticker: Option, 260 | #[serde(skip_serializing_if = "Option::is_none")] 261 | disable_notification: Option, 262 | #[serde(skip_serializing_if = "Option::is_none")] 263 | reply_to_message_id: Option, 264 | #[serde(skip_serializing_if = "Option::is_none")] 265 | reply_markup: Option, 266 | } 267 | 268 | /// Use this method to send video files, Telegram clients support mp4 videos (other formats may be 269 | /// sent as Document). On success, the sent Message is returned. Bots can currently send video 270 | /// files of up to 50 MB in size, this limit may be changed in the future. 271 | #[derive(TelegramFunction, Serialize)] 272 | #[call = "sendVideo"] 273 | #[answer = "Message"] 274 | #[function = "video"] 275 | #[file_kind = "video"] 276 | pub struct SendVideo { 277 | chat_id: Integer, 278 | #[serde(skip_serializing_if = "Option::is_none")] 279 | video: Option, 280 | #[serde(skip_serializing_if = "Option::is_none")] 281 | duration: Option, 282 | #[serde(skip_serializing_if = "Option::is_none")] 283 | width: Option, 284 | #[serde(skip_serializing_if = "Option::is_none")] 285 | height: Option, 286 | #[serde(skip_serializing_if = "Option::is_none")] 287 | caption: Option, 288 | #[serde(skip_serializing_if = "Option::is_none")] 289 | parse_mode: Option, 290 | #[serde(skip_serializing_if = "Option::is_none")] 291 | disable_notification: Option, 292 | #[serde(skip_serializing_if = "Option::is_none")] 293 | reply_to_message_id: Option, 294 | #[serde(skip_serializing_if = "Option::is_none")] 295 | reply_markup: Option, 296 | } 297 | 298 | /// Use this method to send audio files, if you want Telegram clients to display the file as a 299 | /// playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS 300 | /// (other formats may be sent as Audio or Document). On success, the sent Message is returned. 301 | /// Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the 302 | /// future. 303 | #[derive(TelegramFunction, Serialize)] 304 | #[call = "sendVoice"] 305 | #[answer = "Message"] 306 | #[function = "voice"] 307 | #[file_kind = "voice"] 308 | pub struct SendVoice { 309 | chat_id: Integer, 310 | #[serde(skip_serializing_if = "Option::is_none")] 311 | voice: Option, 312 | #[serde(skip_serializing_if = "Option::is_none")] 313 | caption: Option, 314 | #[serde(skip_serializing_if = "Option::is_none")] 315 | parse_mode: Option, 316 | #[serde(skip_serializing_if = "Option::is_none")] 317 | duration: Option, 318 | #[serde(skip_serializing_if = "Option::is_none")] 319 | disable_notification: Option, 320 | #[serde(skip_serializing_if = "Option::is_none")] 321 | reply_to_message_id: Option, 322 | #[serde(skip_serializing_if = "Option::is_none")] 323 | reply_markup: Option, 324 | } 325 | 326 | #[derive(TelegramFunction, Serialize)] 327 | #[call = "sendMediaGroup"] 328 | #[answer = "Messages"] 329 | #[function = "mediagroup"] 330 | #[file_kind = "media"] 331 | pub struct SendMediaGroup { 332 | chat_id: Integer, 333 | media: Option, 334 | #[serde(skip_serializing_if = "Option::is_none")] 335 | disable_notification: Option, 336 | #[serde(skip_serializing_if = "Option::is_none")] 337 | reply_to_message_id: Option 338 | } 339 | 340 | /// Use this method to send point on the map. On success, the sent Message is returned. 341 | #[derive(TelegramFunction, Serialize)] 342 | #[call = "sendLocation"] 343 | #[answer = "Message"] 344 | #[function = "location"] 345 | pub struct SendLocation { 346 | chat_id: Integer, 347 | latitude: f32, 348 | longitude: f32, 349 | #[serde(skip_serializing_if = "Option::is_none")] 350 | disable_notification: Option, 351 | #[serde(skip_serializing_if = "Option::is_none")] 352 | reply_to_message_id: Option, 353 | #[serde(skip_serializing_if = "Option::is_none")] 354 | reply_markup: Option, 355 | } 356 | 357 | /// Use this method to send information about a venue. On success, the sent Message is returned. 358 | #[derive(TelegramFunction, Serialize)] 359 | #[call = "sendVenue"] 360 | #[answer = "Message"] 361 | #[function = "venue"] 362 | pub struct SendVenue { 363 | chat_id: Integer, 364 | latitude: f32, 365 | longitude: f32, 366 | title: String, 367 | address: String, 368 | #[serde(skip_serializing_if = "Option::is_none")] 369 | foursquare_id: Option, 370 | #[serde(skip_serializing_if = "Option::is_none")] 371 | disable_notification: Option, 372 | #[serde(skip_serializing_if = "Option::is_none")] 373 | reply_to_message_id: Option, 374 | #[serde(skip_serializing_if = "Option::is_none")] 375 | reply_markup: Option, 376 | } 377 | 378 | /// Use this method to send phone contacts. On success, the sent Message is returned. 379 | #[derive(TelegramFunction, Serialize)] 380 | #[call = "sendContact"] 381 | #[answer = "Message"] 382 | #[function = "contact"] 383 | pub struct SendContact { 384 | chat_id: Integer, 385 | phone_number: String, 386 | first_name: String, 387 | last_name: Option, 388 | #[serde(skip_serializing_if = "Option::is_none")] 389 | disable_notification: Option, 390 | #[serde(skip_serializing_if = "Option::is_none")] 391 | reply_to_message_id: Option, 392 | #[serde(skip_serializing_if = "Option::is_none")] 393 | reply_markup: Option, 394 | } 395 | 396 | /// Use this method when you need to tell the user that something is happening on the bot's side. 397 | /// The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients 398 | /// clear its typing status). Returns True on success. 399 | #[derive(TelegramFunction, Serialize)] 400 | #[call = "sendChatAction"] 401 | #[answer = "Boolean"] 402 | #[function = "chat_action"] 403 | pub struct SendAction { 404 | chat_id: Integer, 405 | action: String, 406 | } 407 | 408 | /// Use this method to send a game. On success, the sent Message is returned. 409 | #[derive(TelegramFunction, Serialize)] 410 | #[call = "sendGame"] 411 | #[answer = "Message"] 412 | #[function = "send_game"] 413 | pub struct SendGame { 414 | chat_id: Integer, 415 | game_short_name: String, 416 | #[serde(skip_serializing_if = "Option::is_none")] 417 | disable_notification: Option, 418 | #[serde(skip_serializing_if = "Option::is_none")] 419 | reply_to_message_id: Option, 420 | #[serde(skip_serializing_if = "Option::is_none")] 421 | reply_markup: Option, 422 | } 423 | 424 | /// Use this method to set the score of the specified user in a game. On success, if the message 425 | /// was sent by the bot, returns the edited Message, otherwise returns True. Returns an error, if 426 | /// the new score is not greater than the user's current score in the chat and force is False. 427 | #[derive(TelegramFunction, Serialize)] 428 | #[call = "setGameScore"] 429 | #[answer = "Message"] 430 | #[function = "set_game_score"] 431 | pub struct SetGameScore { 432 | user_id: Integer, 433 | score: Integer, 434 | #[serde(skip_serializing_if = "Option::is_none")] 435 | force: Option, 436 | #[serde(skip_serializing_if = "Option::is_none")] 437 | disable_edit_message: Option, 438 | #[serde(skip_serializing_if = "Option::is_none")] 439 | chat_id: Option, 440 | #[serde(skip_serializing_if = "Option::is_none")] 441 | message_id: Option, 442 | #[serde(skip_serializing_if = "Option::is_none")] 443 | inline_message_id: Option, 444 | } 445 | 446 | /// Use this method to get data for high score tables. Will return the score of the specified user 447 | /// and several of his neighbors in a game. On success, returns an Array of GameHighScore objects. 448 | /// 449 | /// This method will currently return scores for the target user, plus two of his closest neighbors 450 | /// on each side. Will also return the top three users if the user and his neighbors are not among 451 | /// them. Please note that this behavior is subject to change. 452 | #[derive(TelegramFunction, Serialize)] 453 | #[call = "getGameHighScores"] 454 | #[answer = "GameHighScore"] 455 | #[function = "get_game_high_scores"] 456 | pub struct GetGameHighScores { 457 | user_id: Integer, 458 | #[serde(skip_serializing_if = "Option::is_none")] 459 | chat_id: Option, 460 | #[serde(skip_serializing_if = "Option::is_none")] 461 | message_id: Option, 462 | #[serde(skip_serializing_if = "Option::is_none")] 463 | inline_message_id: Option, 464 | } 465 | 466 | /// Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos 467 | /// object. 468 | #[derive(TelegramFunction, Serialize)] 469 | #[call = "getUserProfilePhotos"] 470 | #[answer = "UserProfilePhotos"] 471 | #[function = "get_user_profile_photos"] 472 | pub struct GetUserProfilePhotos { 473 | user_id: Integer, 474 | #[serde(skip_serializing_if = "Option::is_none")] 475 | offset: Option, 476 | #[serde(skip_serializing_if = "Option::is_none")] 477 | limit: Option, 478 | } 479 | 480 | /// Use this method to get basic info about a file and prepare it for downloading. For the moment, 481 | /// bots can download files of up to 20MB in size. On success, a File object is returned. The file 482 | /// can then be downloaded via the link https://api.telegram.org/file/bot/, where 483 | /// is taken from the response. It is guaranteed that the link will be valid for at 484 | /// least 1 hour. When the link expires, a new one can be requested by calling getFile again. 485 | #[derive(TelegramFunction, Serialize)] 486 | #[call = "getFile"] 487 | #[answer = "File"] 488 | #[function = "get_file"] 489 | pub struct GetFile { 490 | file_id: String, 491 | } 492 | 493 | /// Use this method to kick a user from a group or a supergroup. In the case of supergroups, the 494 | /// user will not be able to return to the group on their own using invite links, etc., unless 495 | /// unbanned first. The bot must be an administrator in the group for this to work. Returns True on 496 | /// success. 497 | #[derive(TelegramFunction, Serialize)] 498 | #[call = "kickChatMember"] 499 | #[answer = "Boolean"] 500 | #[function = "kick_chat_member"] 501 | pub struct KickChatMember { 502 | chat_id: Integer, 503 | user_id: Integer, 504 | } 505 | 506 | /// Use this method for your bot to leave a group, supergroup or channel. Returns True on 507 | /// success. 508 | #[derive(TelegramFunction, Serialize)] 509 | #[call = "leaveChat"] 510 | #[answer = "Boolean"] 511 | #[function = "leave_chat"] 512 | pub struct LeaveChat { 513 | chat_id: Integer, 514 | } 515 | 516 | /// Use this method to unban a previously kicked user in a supergroup. The user will not return to 517 | /// the group automatically, but will be able to join via link, etc. The bot must be an 518 | /// administrator in the group for this to work. Returns True on success. 519 | #[derive(TelegramFunction, Serialize)] 520 | #[call = "unbanChatMember"] 521 | #[answer = "Boolean"] 522 | #[function = "unban_chat_member"] 523 | pub struct UnbanChatMember { 524 | chat_id: Integer, 525 | user_id: Integer, 526 | } 527 | 528 | /// Use this method to restrict a user in a supergroup. The bot must be an administrator in the 529 | /// supergroup for this to work and must have the appropriate admin rights. Pass True for all 530 | /// boolean parameters to lift restrictions from a user. Returns True on success. 531 | #[derive(TelegramFunction, Serialize)] 532 | #[call = "restrictChatMember"] 533 | #[answer = "Boolean"] 534 | #[function = "restrict_chat_member"] 535 | pub struct RestrictChatMember { 536 | chat_id: Integer, 537 | user_id: Integer, 538 | #[serde(skip_serializing_if = "Option::is_none")] 539 | until_date: Option, 540 | #[serde(skip_serializing_if = "Option::is_none")] 541 | can_send_messages: Option, 542 | #[serde(skip_serializing_if = "Option::is_none")] 543 | can_send_media_messages: Option, 544 | #[serde(skip_serializing_if = "Option::is_none")] 545 | can_send_other_messages: Option, 546 | #[serde(skip_serializing_if = "Option::is_none")] 547 | can_add_web_previews: Option, 548 | } 549 | 550 | /// Use this method to promote or demote a user in a supergroup or a channel. The bot must be an 551 | /// administrator in the chat for this to work and must have the appropriate admin rights. Pass 552 | /// False for all boolean parameters to demote a user. Returns True on success. 553 | #[derive(TelegramFunction, Serialize)] 554 | #[call = "promoteChatMember"] 555 | #[answer = "Boolean"] 556 | #[function = "promote_chat_member"] 557 | pub struct PromoteChatMember { 558 | chat_id: Integer, 559 | user_id: Integer, 560 | #[serde(skip_serializing_if = "Option::is_none")] 561 | can_change_into: Option, 562 | #[serde(skip_serializing_if = "Option::is_none")] 563 | can_post_messages: Option, 564 | #[serde(skip_serializing_if = "Option::is_none")] 565 | can_edit_messages: Option, 566 | #[serde(skip_serializing_if = "Option::is_none")] 567 | can_delete_messages: Option, 568 | #[serde(skip_serializing_if = "Option::is_none")] 569 | can_invite_users: Option, 570 | #[serde(skip_serializing_if = "Option::is_none")] 571 | can_restrict_members: Option, 572 | #[serde(skip_serializing_if = "Option::is_none")] 573 | can_pin_messages: Option, 574 | #[serde(skip_serializing_if = "Option::is_none")] 575 | can_promote_members: Option, 576 | } 577 | 578 | /// Use this method to generate a new invite link for a chat; any previously generated link is 579 | /// revoked. The bot must be an administrator in the chat for this to work and must have the 580 | /// appropriate admin rights. Returns the new invite link as String on success. 581 | #[derive(TelegramFunction, Serialize)] 582 | #[call = "exportChatInviteLink"] 583 | #[answer = "Link"] 584 | #[function = "export_chat_invite_link"] 585 | pub struct ExportChatInviteLink { 586 | chat_id: Integer, 587 | } 588 | 589 | /// Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must 590 | /// be an administrator in the chat for this to work and must have the appropriate admin rights. 591 | /// Returns True on success. 592 | /// 593 | /// Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are 594 | /// Admins’ setting is off in the target group. 595 | #[derive(TelegramFunction, Serialize)] 596 | #[call = "deleteChatPhoto"] 597 | #[answer = "Boolean"] 598 | #[function = "delete_chat_photo"] 599 | pub struct DeleteChatPhoto { 600 | chat_id: Integer, 601 | } 602 | 603 | /// Use this method to change the title of a chat. Titles can't be changed for private chats. The 604 | /// bot must be an administrator in the chat for this to work and must have the appropriate admin 605 | /// rights. Returns True on success. 606 | /// 607 | /// Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are 608 | /// Admins’ setting is off in the target group. 609 | #[derive(TelegramFunction, Serialize)] 610 | #[call = "setChatTitle"] 611 | #[answer = "Boolean"] 612 | #[function = "set_chat_title"] 613 | pub struct SetChatTitle { 614 | chat_id: Integer, 615 | title: String, 616 | } 617 | 618 | /// Use this method to change the description of a supergroup or a channel. The bot must be an 619 | /// administrator in the chat for this to work and must have the appropriate admin rights. Returns 620 | /// True on success. 621 | #[derive(TelegramFunction, Serialize)] 622 | #[call = "setChatDescription"] 623 | #[answer = "Boolean"] 624 | #[function = "set_chat_description"] 625 | pub struct SetChatDescription { 626 | chat_id: Integer, 627 | description: String, 628 | } 629 | 630 | /// Use this method to pin a message in a supergroup or a channel. The bot must be an administrator 631 | /// in the chat for this to work and must have the ‘can_pin_messages’ admin right in the supergroup 632 | /// or ‘can_edit_messages’ admin right in the channel. Returns True on success. 633 | #[derive(TelegramFunction, Serialize)] 634 | #[call = "pinChatMessage"] 635 | #[answer = "Boolean"] 636 | #[function = "pin_chat_message"] 637 | pub struct PinChatMessage { 638 | chat_id: Integer, 639 | message_id: Integer, 640 | #[serde(skip_serializing_if = "Option::is_none")] 641 | disable_notification: Option, 642 | } 643 | 644 | /// Use this method to unpin a message in a supergroup or a channel. The bot must be an 645 | /// administrator in the chat for this to work and must have the ‘can_pin_messages’ admin right in 646 | /// the supergroup or ‘can_edit_messages’ admin right in the channel. Returns True on success. 647 | #[derive(TelegramFunction, Serialize)] 648 | #[call = "unpinChatMessage"] 649 | #[answer = "Boolean"] 650 | #[function = "unpin_chat_message"] 651 | pub struct UnpinChatMessage { 652 | chat_id: Integer, 653 | } 654 | 655 | /// Use this method to get up to date information about the chat (current name of the user for 656 | /// one-on-one conversations, current username of a user, group or channel, etc.). Returns a Chat 657 | /// object on success. 658 | #[derive(TelegramFunction, Serialize)] 659 | #[call = "getChat"] 660 | #[answer = "Chat"] 661 | #[function = "get_chat"] 662 | pub struct GetChat { 663 | chat_id: Integer, 664 | } 665 | 666 | /// Use this method to get a list of administrators in a chat. On success, returns an Array of 667 | /// ChatMember objects that contains information about all chat administrators except other bots. 668 | /// If the chat is a group or a supergroup and no administrators were appointed, only the creator 669 | /// will be returned. 670 | #[derive(TelegramFunction, Serialize)] 671 | #[call = "getChatAdministrators"] 672 | #[answer = "Vector"] 673 | #[function = "unban_chat_administrators"] 674 | pub struct GetChatAdministrators { 675 | chat_id: Integer, 676 | } 677 | 678 | /// Use this method to get the number of members in a chat. Returns Int on success. 679 | #[derive(TelegramFunction, Serialize)] 680 | #[call = "getChatMembersCount"] 681 | #[answer = "Integer"] 682 | #[function = "get_chat_members_count"] 683 | pub struct GetChatMemberCounts { 684 | chat_id: Integer, 685 | } 686 | 687 | /// Use this method to get information about a member of a chat. Returns a ChatMember object on 688 | /// success. 689 | #[derive(TelegramFunction, Serialize)] 690 | #[call = "getChatMember"] 691 | #[answer = "ChatMember"] 692 | #[function = "get_chat_member"] 693 | pub struct GetChatMember { 694 | chat_id: Integer, 695 | user_id: Integer, 696 | } 697 | 698 | /// Use this method to send answers to callback queries sent from inline keyboards. The answer will 699 | /// be displayed to the user as a notification at the top of the chat screen or as an alert. On 700 | /// success, True is returned. 701 | #[derive(TelegramFunction, Serialize)] 702 | #[call = "answerCallbackQuery"] 703 | #[answer = "Boolean"] 704 | #[function = "answer_callback_query"] 705 | pub struct AnswerCallbackQuery { 706 | callback_query_id: String, 707 | #[serde(skip_serializing_if = "Option::is_none")] 708 | text: Option, 709 | #[serde(skip_serializing_if = "Option::is_none")] 710 | show_alert: Option, 711 | #[serde(skip_serializing_if = "Option::is_none")] 712 | url: Option, 713 | #[serde(skip_serializing_if = "Option::is_none")] 714 | cache_time: Option, 715 | } 716 | 717 | /// Use this method to send answers to an inline query. On success, True is returned. 718 | /// No more than 50 results per query are allowed. 719 | #[derive(TelegramFunction, Serialize)] 720 | #[call = "answerInlineQuery"] 721 | #[answer = "Boolean"] 722 | #[function = "answer_inline_query"] 723 | pub struct AnswerInlineQuery { 724 | inline_query_id: String, 725 | results: Vec>, 726 | #[serde(skip_serializing_if = "Option::is_none")] 727 | cache_time: Option, 728 | #[serde(skip_serializing_if = "Option::is_none")] 729 | is_personal: Option, 730 | #[serde(skip_serializing_if = "Option::is_none")] 731 | next_offset: Option, 732 | #[serde(skip_serializing_if = "Option::is_none")] 733 | switch_pm_text: Option, 734 | #[serde(skip_serializing_if = "Option::is_none")] 735 | switch_pm_parameter: Option, 736 | } 737 | 738 | /// Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). 739 | /// On success, if edited message is sent by the bot, the edited Message is returned, otherwise True 740 | /// is returned. 741 | #[derive(TelegramFunction, Serialize)] 742 | #[call = "editMessageText"] 743 | #[answer = "EditResponse"] 744 | #[function = "edit_message_text"] 745 | pub struct EditMessageText { 746 | text: String, 747 | #[serde(skip_serializing_if = "Option::is_none")] 748 | chat_id: Option, 749 | #[serde(skip_serializing_if = "Option::is_none")] 750 | message_id: Option, 751 | #[serde(skip_serializing_if = "Option::is_none")] 752 | inline_message_id: Option, 753 | #[serde(skip_serializing_if = "Option::is_none")] 754 | parse_mode: Option, 755 | #[serde(skip_serializing_if = "Option::is_none")] 756 | disable_web_page_preview: Option, 757 | #[serde(skip_serializing_if = "Option::is_none")] 758 | reply_markup: Option, 759 | } 760 | 761 | /// Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). 762 | /// On success, if edited message is sent by the bot, the edited Message is returned, otherwise 763 | /// True is returned. 764 | #[derive(TelegramFunction, Serialize)] 765 | #[call = "editMessageCaption"] 766 | #[answer = "EditResponse"] 767 | #[function = "edit_message_caption"] 768 | pub struct EditMessageCaption { 769 | #[serde(skip_serializing_if = "Option::is_none")] 770 | chat_id: Option, 771 | #[serde(skip_serializing_if = "Option::is_none")] 772 | message_id: Option, 773 | #[serde(skip_serializing_if = "Option::is_none")] 774 | inline_message_id: Option, 775 | #[serde(skip_serializing_if = "Option::is_none")] 776 | caption: Option, 777 | #[serde(skip_serializing_if = "Option::is_none")] 778 | parse_mode: Option, 779 | #[serde(skip_serializing_if = "Option::is_none")] 780 | reply_markup: Option, 781 | } 782 | 783 | /// Use this method to edit only the reply markup of messages sent by the bot or via the bot (for 784 | /// inline bots). On success, if edited message is sent by the bot, the edited Message is returned, 785 | /// otherwise True is returned. 786 | #[derive(TelegramFunction, Serialize)] 787 | #[call = "editMessageReplyMarkup"] 788 | #[answer = "EditResponse"] 789 | #[function = "edit_message_reply_markup"] 790 | pub struct EditMessageReplyMarkup { 791 | #[serde(skip_serializing_if = "Option::is_none")] 792 | chat_id: Option, 793 | #[serde(skip_serializing_if = "Option::is_none")] 794 | message_id: Option, 795 | #[serde(skip_serializing_if = "Option::is_none")] 796 | inline_message_id: Option, 797 | #[serde(skip_serializing_if = "Option::is_none")] 798 | reply_markup: Option, 799 | } 800 | 801 | /// Use this method to delete a message, including service messages, with the following limitations: 802 | /// - A message can only be deleted if it was sent less than 48 hours ago. 803 | /// - Bots can delete outgoing messages in groups and supergroups. 804 | /// - Bots granted can_post_messages permissions can delete outgoing messages in channels. 805 | /// - If the bot is an administrator of a group, it can delete any message there. 806 | /// - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any 807 | /// message there. 808 | /// Returns True on success. 809 | #[derive(TelegramFunction, Serialize)] 810 | #[call = "deleteMessage"] 811 | #[answer = "Boolean"] 812 | #[function = "delete_message"] 813 | pub struct DeleteMessage { 814 | chat_id: Integer, 815 | message_id: Integer, 816 | } 817 | 818 | ///Use this method to create new sticker set owned by a user. 819 | ///The bot will be able to edit the created sticker set. Returns True on success. 820 | #[derive(TelegramFunction, Serialize)] 821 | #[call = "createNewStickerSet"] 822 | #[answer = "Boolean"] 823 | #[function = "create_new_sticker_set"] 824 | #[file_kind = "png_sticker"] 825 | pub struct CreateNewStickerSet { 826 | user_id: Integer, 827 | name: String, 828 | title: String, 829 | emojis: String, 830 | #[serde(skip_serializing_if = "Option::is_none")] 831 | png_sticker: Option, 832 | } 833 | 834 | ///Use this method to add a new sticker to a set created by the bot. Returns True on success. 835 | #[derive(TelegramFunction, Serialize)] 836 | #[call = "addStickerToSet"] 837 | #[answer = "Boolean"] 838 | #[function = "add_sticker_to_set"] 839 | #[file_kind = "png_sticker"] 840 | pub struct AddStickerToSet { 841 | user_id: Integer, 842 | name: String, 843 | emojis: String, 844 | #[serde(skip_serializing_if = "Option::is_none")] 845 | png_sticker: Option, 846 | } 847 | 848 | ///Use this method to add a new sticker to a set created by the bot. Returns True on success. 849 | #[derive(TelegramFunction, Serialize)] 850 | #[call = "deleteStickerFromSet"] 851 | #[answer = "Boolean"] 852 | #[function = "delete_sticker_from_set"] 853 | pub struct DeleteStickerFromSet { 854 | sticker: String, 855 | } 856 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Write a telegram bot in Rust 2 | //! 3 | //! This library allows you to write a Telegram Bot in Rust. It's an almost complete wrapper for the Telegram Bot API and uses hyper to send a request to the Telegram server. Each Telegram function call returns a future and carries the actual bot and the answer. 4 | //! You can find all available functions in src/functions.rs. The crate telebot-derive implements all 5 | //! required getter, setter and send functions automatically. 6 | //! 7 | //! # Example usage 8 | //! 9 | //! ``` 10 | //! use telebot::Bot; 11 | //! use futures::stream::Stream; 12 | //! use std::env; 13 | //! 14 | //! // import all available functions 15 | //! use telebot::functions::*; 16 | //! 17 | //! fn main() { 18 | //! // Create the bot 19 | //! let mut bot = Bot::new(&env::var("TELEGRAM_BOT_KEY").unwrap()).update_interval(200); 20 | //! 21 | //! // Register a reply command which answers a message 22 | //! let handle = bot.new_cmd("/reply") 23 | //! .and_then(|(bot, msg)| { 24 | //! let mut text = msg.text.unwrap().clone(); 25 | //! if text.is_empty() { 26 | //! text = "".into(); 27 | //! } 28 | //! 29 | //! bot.message(msg.chat.id, text).send() 30 | //! }) 31 | //! .for_each(|_| Ok(())); 32 | //! 33 | //! bot.run_with(handle); 34 | //! } 35 | //! ``` 36 | 37 | #![allow(bare_trait_objects)] 38 | #![allow(unused_attributes)] 39 | 40 | #[macro_use] 41 | extern crate telebot_derive; 42 | 43 | #[macro_use] 44 | extern crate log; 45 | 46 | extern crate hyper_multipart_rfc7578 as hyper_multipart; 47 | #[macro_use] 48 | extern crate serde; 49 | 50 | extern crate tokio; 51 | 52 | pub use bot::Bot; 53 | pub use error::Error; 54 | pub use file::File; 55 | 56 | pub mod bot; 57 | pub mod error; 58 | pub mod objects; 59 | pub mod functions; 60 | pub mod file; 61 | -------------------------------------------------------------------------------- /src/objects.rs: -------------------------------------------------------------------------------- 1 | //! The complete list of telegram types, copied from: 2 | //! https://core.telegram.org/bots/api#available-types 3 | //! 4 | //! on each struct getter, setter and send function will be implemented 5 | 6 | /// These objects are redefinitions of basic types. telebot-derive will scope every object in 7 | /// answer, so we need to redefine them here. 8 | pub type Boolean = bool; 9 | pub type Integer = i64; 10 | pub type Vector = Vec; 11 | pub type NotImplemented = (); 12 | 13 | use erased_serde::Serialize; 14 | use uuid::Uuid; 15 | 16 | #[derive(Deserialize, Debug)] 17 | #[serde(untagged)] 18 | pub enum EditResponse { 19 | Message(Message), 20 | Boolean(Boolean), 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug, Clone)] 24 | pub struct Link(pub String); 25 | 26 | /// This object represents a Telegram user or bot. 27 | #[derive(Serialize, Deserialize, Debug, Clone)] 28 | pub struct User { 29 | pub id: Integer, 30 | pub first_name: String, 31 | pub last_name: Option, 32 | pub username: Option, 33 | pub language_code: Option, 34 | pub is_bot: Option, 35 | } 36 | 37 | /// This object represents a chat. 38 | #[derive(Deserialize, Debug)] 39 | pub struct Chat { 40 | pub id: Integer, 41 | #[serde(rename = "type")] 42 | pub kind: String, 43 | pub title: Option, 44 | pub username: Option, 45 | pub first_name: Option, 46 | pub last_name: Option, 47 | pub all_members_are_administrators: Option, 48 | } 49 | 50 | /// This object represents one special entity in a text message. For example, hashtags, usernames, 51 | /// URLs, etc. 52 | #[derive(Deserialize, Debug)] 53 | pub struct MessageEntity { 54 | #[serde(rename = "type")] 55 | pub kind: String, 56 | pub offset: Integer, 57 | pub length: Integer, 58 | pub url: Option, 59 | pub user: Option, 60 | } 61 | 62 | /// This object represents a message. 63 | #[derive(Deserialize, Debug)] 64 | pub struct Message { 65 | pub message_id: Integer, 66 | pub from: Option, 67 | pub date: Integer, 68 | pub chat: Chat, 69 | pub forward_from: Option, 70 | pub forward_from_chat: Option, 71 | pub forward_from_message_id: Option, 72 | pub forward_date: Option, 73 | pub reply_to_message: Option>, 74 | pub edit_date: Option, 75 | pub text: Option, 76 | pub entities: Option>, 77 | pub audio: Option