Merge pull request #158 from teloxide/dev

Dev
This commit is contained in:
Temirkhan Myrzamadi 2020-02-14 17:24:17 +06:00 committed by GitHub
commit 75cff40eb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
223 changed files with 21373 additions and 78 deletions

72
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,72 @@
on: [push, pull_request]
name: Continuous integration
jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt, clippy
- name: stable/beta build
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: build
args: --verbose --features ""
- name: nightly build
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: build
args: --verbose --all-features
- name: stable/beta test
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: test
args: --verbose --features ""
- name: nightly test
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: test
args: --verbose --all-features
- name: fmt
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: fmt
args: --all -- --check
- name: stable/beta clippy
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: clippy
args: --all-targets --features "" -- -D warnings
- name: nightly clippy
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: clippy
args: --all-targets --all-features -- -D warnings

9
.gitignore vendored
View file

@ -1,4 +1,11 @@
/target
**/*.rs.bk
Cargo.lock
.idea/
.idea/
.vscode/
examples/ping_pong_bot/target
examples/dialogue_bot/target
examples/multiple_handlers_bot/target
examples/admin_bot/target
examples/guess_a_number_bot/target
examples/simple_commands_bot/target

124
CODE_STYLE.md Normal file
View file

@ -0,0 +1,124 @@
# Code style
This is a description of a coding style that every contributor must follow. Please, read the whole document before you start pushing code.
## Generics
Generics are always written with `where`.
Bad:
```rust
pub fn new<N: Into<String>,
T: Into<String>,
P: Into<InputFile>,
E: Into<String>>
(user_id: i32, name: N, title: T, png_sticker: P, emojis: E) -> Self { ... }
```
Good:
```rust
pub fn new<N, T, P, E>(user_id: i32, name: N, title: T, png_sticker: P, emojis: E) -> Self
where
N: Into<String>,
T: Into<String>,
P: Into<InputFile>,
E: Into<String> { ... }
```
## Comments
1. Comments must describe what your code does and mustn't describe how your code does it and bla-bla-bla. Be sure that your comments follow the grammar, including punctuation, the first capital letter and so on.
Bad:
```rust
/// this function make request to telegram
pub fn make_request(url: &str) -> String { ... }
```
Good:
```rust
/// This function makes a request to Telegram.
pub fn make_request(url: &str) -> String { ... }
```
2. Also, link resources in your comments when possible:
```rust
/// Download a file from Telegram.
///
/// `path` can be obtained from the [`Bot::get_file`].
///
/// To download into [`AsyncWrite`] (e.g. [`tokio::fs::File`]), see
/// [`Bot::download_file`].
///
/// [`Bot::get_file`]: crate::bot::Bot::get_file
/// [`AsyncWrite`]: tokio::io::AsyncWrite
/// [`tokio::fs::File`]: tokio::fs::File
/// [`Bot::download_file`]: crate::Bot::download_file
#[cfg(feature = "unstable-stream")]
pub async fn download_file_stream(
&self,
path: &str,
) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>>, reqwest::Error>
{
download_file_stream(&self.client, &self.token, path).await
}
```
## Use Self where possible
Bad:
```rust
impl ErrorKind {
fn print(&self) {
ErrorKind::Io => println!("Io"),
ErrorKind::Network => println!("Network"),
ErrorKind::Json => println!("Json"),
}
}
```
Good:
```rust
impl ErrorKind {
fn print(&self) {
Self::Io => println!("Io"),
Self::Network => println!("Network"),
Self::Json => println!("Json"),
}
}
```
<details>
<summary>More examples</summary>
Bad:
```rust
impl<'a> AnswerCallbackQuery<'a> {
pub(crate) fn new<C>(bot: &'a Bot, callback_query_id: C) -> AnswerCallbackQuery<'a>
where
C: Into<String>, { ... }
```
Good:
```rust
impl<'a> AnswerCallbackQuery<'a> {
pub(crate) fn new<C>(bot: &'a Bot, callback_query_id: C) -> Self
where
C: Into<String>, { ... }
```
</details>
## Naming
1. Avoid unnecessary duplication (`Message::message_id` -> `Message::id` using `#[serde(rename = "message_id")]`).
2. Use a generic parameter name `S` for streams, `Fut` for futures, `F` for functions (where possible).
## Deriving
1. Derive `Copy`, `Eq`, `Hash`, `PartialEq`, `Clone`, `Debug` for public types when possible (note: if the default `Debug` implementation is weird, you should manually implement it by yourself).
2. Derive `Default` when there is an algorithm to get a default value for your type.
## Misc
1. Use `Into<...>` only where there exists at least one conversion **and** it will be logically to use.

13
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,13 @@
# Contributing
Before contributing, please read our [the code style](https://github.com/teloxide/teloxide/blob/dev/CODE_STYLE.md).
To change the source code, fork this repository and work inside your own branch. Then send us a PR and wait for the CI to check everything. However, you'd better check changes first locally:
```
cargo clippy --all --all-features --all-targets
cargo test --all
cargo doc --open
cargo fmt --all -- --check
```
To report a bug, suggest new functionality, or ask a question, go to [Issues](https://github.com/teloxide/teloxide/issues). Try to make MRE (**M**inimal **R**eproducible **E**xample) and specify your teloxide version to let others help you.

View file

@ -1,13 +1,42 @@
[package]
name = "async-telegram-bot"
name = "teloxide"
version = "0.1.0"
edition = "2018"
description = "An elegant Telegram bots framework for Rust"
repository = "https://github.com/teloxide/teloxide"
documentation = "https://docs.rs/teloxide/"
readme = "README.md"
keywords = ["teloxide", "telegram", "telegram-bot-framework", "telegram-bot-api"]
license = "MIT"
[badges]
maintenance = { status = "actively-developed" }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures-preview = { version = "0.3.0-alpha.14", features = ["compat"] }
reqwest = "0.9.20"
serde_json = "1.0.39"
serde = {version = "1.0.92", features = ["derive"] }
lazy_static = "1.3"
serde_json = "1.0.44"
serde = { version = "1.0.101", features = ["derive"] }
tokio = { version = "0.2.6", features = ["full"] }
tokio-util = { version = "0.2.0", features = ["full"] }
reqwest = { version = "0.10", features = ["json", "stream", "native-tls-vendored"] }
log = "0.4.8"
bytes = "0.5.3"
mime = "0.3.16"
derive_more = "0.99.2"
thiserror = "1.0.9"
async-trait = "0.1.22"
futures = "0.3.1"
pin-project = "0.4.6"
serde_with_macros = "1.0.1"
either = "1.5.3"
teloxide-macros = { path = "teloxide-macros" }
[dev-dependencies]
smart-default = "0.6.0"
rand = "0.7.3"
pretty_env_logger = "0.4.0"

BIN
ICON.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 async-telegram-bot
Copyright (c) 2019 teloxide
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

246
README.md
View file

@ -1,2 +1,244 @@
# async-telegram-bot
An asynchronous full-featured Telegram bot framework for Rust
<div align="center">
<img src="ICON.png" width="250"/>
<h1>teloxide</h1>
<a href="https://docs.rs/teloxide/">
<img src="https://img.shields.io/badge/docs.rs-v0.1.0-blue.svg">
</a>
<a href="https://github.com/teloxide/teloxide/actions">
<img src="https://github.com/teloxide/teloxide/workflows/Continuous%20integration/badge.svg">
</a>
<a href="https://crates.io/crates/teloxide">
<img src="https://img.shields.io/badge/crates.io-v0.1.0-orange.svg">
</a>
A full-featured framework that empowers you to easily build [Telegram bots](https://telegram.org/blog/bot-revolution) using the [`async`/`.await`](https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html) syntax in [Rust](https://www.rust-lang.org/). It handles all the difficult stuff so you can focus only on your business logic.
</div>
## Features
- **Type-safe.** teloxide leverages the Rust's type system with two serious implications: resistance to human mistakes and tight integration with IDEs. Write fast, avoid debugging as possible.
- **Persistency.** By default, teloxide stores all user dialogues in RAM, but you can store them somewhere else (for example, in DB) just by implementing 2 functions.
- **Convenient dialogues system.** Define a type-safe [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine)
and transition functions to drive a user dialogue with ease (see the examples below).
- **Convenient API.** Automatic conversions are used to avoid boilerplate. For example, functions accept `Into<String>`, rather than `&str` or `String`, so you can call them without `.to_string()`/`.as_str()`/etc.
## Getting started
1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
2. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
```bash
# Unix
$ export TELOXIDE_TOKEN=MyAwesomeToken
# Windows
$ set TELOXITE_TOKEN=MyAwesomeToken
```
3. Be sure that you are up to date:
```bash
$ rustup update stable
```
4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
```toml
[dependencies]
teloxide = "0.1.0"
log = "0.4.8"
tokio = "0.2.11"
pretty_env_logger = "0.4.0"
```
## The ping-pong bot
This bot has a single message handler, which answers "pong" to each incoming message:
([Full](https://github.com/teloxide/teloxide/blob/dev/examples/ping_pong_bot/src/main.rs))
```rust
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting the ping-pong bot!");
let bot = Bot::from_env();
Dispatcher::<RequestError>::new(bot)
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
ctx.answer("pong").send().await?;
Ok(())
})
.dispatch()
.await;
}
```
## Commands
Commands are defined similar to how we define CLI using [structopt](https://docs.rs/structopt/0.3.9/structopt/). This bot says "I am a cat! Meow!" on `/meow`, generates a random number within [0; 1) on `/generate`, and shows the usage guide on `/help`:
([Full](https://github.com/teloxide/teloxide/blob/dev/examples/simple_commands_bot/src/main.rs))
```rust
// Imports are omitted...
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "display this text.")]
Help,
#[command(description = "be a cat.")]
Meow,
#[command(description = "generate a random number within [0; 1).")]
Generate,
}
async fn handle_command(
ctx: DispatcherHandlerCtx<Message>,
) -> Result<(), RequestError> {
let text = match ctx.update.text() {
Some(text) => text,
None => {
log::info!("Received a message, but not text.");
return Ok(());
}
};
let command = match Command::parse(text) {
Some((command, _)) => command,
None => {
log::info!("Received a text message, but not a command.");
return Ok(());
}
};
match command {
Command::Help => ctx.answer(Command::descriptions()).send().await?,
Command::Generate => {
ctx.answer(thread_rng().gen_range(0.0, 1.0).to_string())
.send()
.await?
}
Command::Meow => ctx.answer("I am a cat! Meow!").send().await?,
};
Ok(())
}
#[tokio::main]
async fn main() {
// Setup is omitted...
}
```
## Guess a number
Wanna see more? This is a bot, which starts a game on each incoming message. You must guess a number from 1 to 10 (inclusively):
([Full](https://github.com/teloxide/teloxide/blob/dev/examples/guess_a_number_bot/src/main.rs))
```rust
// Imports are omitted...
#[derive(SmartDefault)]
enum Dialogue {
#[default]
Start,
ReceiveAttempt(u8),
}
async fn handle_message(
ctx: DialogueHandlerCtx<Message, Dialogue>,
) -> Result<DialogueStage<Dialogue>, RequestError> {
match ctx.dialogue {
Dialogue::Start => {
ctx.answer(
"Let's play a game! Guess a number from 1 to 10 (inclusively).",
)
.send()
.await?;
next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
}
Dialogue::ReceiveAttempt(secret) => match ctx.update.text() {
None => {
ctx.answer("Oh, please, send me a text message!")
.send()
.await?;
next(ctx.dialogue)
}
Some(text) => match text.parse::<u8>() {
Ok(attempt) => match attempt {
x if !(1..=10).contains(&x) => {
ctx.answer(
"Oh, please, send me a number in the range [1; \
10]!",
)
.send()
.await?;
next(ctx.dialogue)
}
x if x == secret => {
ctx.answer("Congratulations! You won!").send().await?;
exit()
}
_ => {
ctx.answer("No.").send().await?;
next(ctx.dialogue)
}
},
Err(_) => {
ctx.answer(
"Oh, please, send me a number in the range [1; 10]!",
)
.send()
.await?;
next(ctx.dialogue)
}
},
},
}
}
#[tokio::main]
async fn main() {
// Setup is omitted...
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
}))
.dispatch()
.await;
}
```
Our [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine), designating a user dialogue, cannot be in an invalid state. See [examples/dialogue_bot](https://github.com/teloxide/teloxide/blob/dev/examples/dialogue_bot/src/main.rs) to see a bit more complicated bot with dialogues.
[See more examples](https://github.com/teloxide/teloxide/tree/dev/examples).
## Recommendations
- Use this pattern:
```rust
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
// Your logic here...
}
```
Instead of this:
```rust
#[tokio::main]
async fn main() {
// Your logic here...
}
```
The second one produces very strange compiler messages because of the `#[tokio::main]` macro. However, the examples in this README use the second variant for brevity.
## Contributing
See [CONRIBUTING.md](https://github.com/teloxide/teloxide/blob/dev/CONTRIBUTING.md).

9
examples/README.md Normal file
View file

@ -0,0 +1,9 @@
# Examples
Just enter the directory (for example, `cd dialogue_bot`) and execute `cargo run` to run an example. Don't forget to initialise the `TELOXIDE_TOKEN` environmental variable.
- [ping_pong_bot](ping_pong_bot) - Answers "pong" to each incoming message.
- [simple_commands_bot](simple_commands_bot) - Shows how to deal with bot's commands.
- [guess_a_number_bot](guess_a_number_bot) - The "guess a number" game.
- [dialogue_bot](dialogue_bot) - Drive a dialogue with a user using a type-safe finite automaton.
- [admin_bot](admin_bot) - A bot, which can ban, kick, and mute on a command.
- [multiple_handlers_bot](multiple_handlers_bot) - Shows how multiple dispatcher's handlers relate to each other.

View file

@ -0,0 +1,16 @@
[package]
name = "admin_bot"
version = "0.1.0"
authors = ["p0lunin <dmytro.polunin@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }
[profile.release]
lto = true

View file

@ -0,0 +1,203 @@
// TODO: simplify this and use typed command variants (see https://github.com/teloxide/teloxide/issues/152).
use teloxide::{
prelude::*, types::ChatPermissions, utils::command::BotCommand,
};
// Derive BotCommand to parse text with a command into this enumeration.
//
// 1. rename = "lowercase" turns all the commands into lowercase letters.
// 2. `description = "..."` specifies a text before all the commands.
//
// That is, you can just call Command::descriptions() to get a description of
// your commands in this format:
// %GENERAL-DESCRIPTION%
// %PREFIX%%COMMAND% - %DESCRIPTION%
#[derive(BotCommand)]
#[command(
rename = "lowercase",
description = "Use commands in format /%command% %num% %unit%"
)]
enum Command {
#[command(description = "kick user from chat.")]
Kick,
#[command(description = "ban user in chat.")]
Ban,
#[command(description = "mute user in chat.")]
Mute,
Help,
}
// Calculates time of user restriction.
fn calc_restrict_time(num: i32, unit: &str) -> Result<i32, &str> {
match unit {
"h" | "hours" => Ok(num * 3600),
"m" | "minutes" => Ok(num * 60),
"s" | "seconds" => Ok(num),
_ => Err("Allowed units: h, m, s"),
}
}
// Parse arguments after a command.
fn parse_args(args: Vec<&str>) -> Result<(i32, &str), &str> {
let num = match args.get(0) {
Some(s) => s,
None => return Err("Use command in format /%command% %num% %unit%"),
};
let unit = match args.get(1) {
Some(s) => s,
None => return Err("Use command in format /%command% %num% %unit%"),
};
match num.parse::<i32>() {
Ok(n) => Ok((n, unit)),
Err(_) => Err("input positive number!"),
}
}
// Parse arguments into a user restriction duration.
fn parse_time_restrict(args: Vec<&str>) -> Result<i32, &str> {
let (num, unit) = parse_args(args)?;
calc_restrict_time(num, unit)
}
type Ctx = DispatcherHandlerCtx<Message>;
// Mute a user with a replied message.
async fn mute_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
Some(msg1) => match parse_time_restrict(args) {
// Mute user temporarily...
Ok(time) => {
ctx.bot
.restrict_chat_member(
ctx.update.chat_id(),
msg1.from().expect("Must be MessageKind::Common").id,
ChatPermissions::default(),
)
.until_date(ctx.update.date + time)
.send()
.await?;
}
// ...or permanently
Err(_) => {
ctx.bot
.restrict_chat_member(
ctx.update.chat_id(),
msg1.from().unwrap().id,
ChatPermissions::default(),
)
.send()
.await?;
}
},
None => {
ctx.reply_to("Use this command in reply to another message")
.send()
.await?;
}
}
Ok(())
}
// Kick a user with a replied message.
async fn kick_user(ctx: &Ctx) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
Some(mes) => {
// bot.unban_chat_member can also kicks a user from a group chat.
ctx.bot
.unban_chat_member(ctx.update.chat_id(), mes.from().unwrap().id)
.send()
.await?;
}
None => {
ctx.reply_to("Use this command in reply to another message")
.send()
.await?;
}
}
Ok(())
}
// Ban a user with replied message.
async fn ban_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
Some(message) => match parse_time_restrict(args) {
// Mute user temporarily...
Ok(time) => {
ctx.bot
.kick_chat_member(
ctx.update.chat_id(),
message.from().expect("Must be MessageKind::Common").id,
)
.until_date(ctx.update.date + time)
.send()
.await?;
}
// ...or permanently
Err(_) => {
ctx.bot
.kick_chat_member(
ctx.update.chat_id(),
message.from().unwrap().id,
)
.send()
.await?;
}
},
None => {
ctx.reply_to("Use this command in a reply to another message!")
.send()
.await?;
}
}
Ok(())
}
// Handle all messages.
async fn handle_command(ctx: Ctx) -> Result<(), RequestError> {
if ctx.update.chat.is_group() {
// The same as DispatcherHandlerResult::exit(Ok(())). If you have more
// handlers, use DispatcherHandlerResult::next(...)
return Ok(());
}
if let Some(text) = ctx.update.text() {
// Parse text into a command with args.
let (command, args): (Command, Vec<&str>) = match Command::parse(text) {
Some(tuple) => tuple,
None => return Ok(()),
};
match command {
Command::Help => {
ctx.answer(Command::descriptions()).send().await?;
}
Command::Kick => {
kick_user(&ctx).await?;
}
Command::Ban => {
ban_user(&ctx, args).await?;
}
Command::Mute => {
mute_user(&ctx, args).await?;
}
};
}
Ok(())
}
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting admin_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&handle_command)
.dispatch()
.await
}

View file

@ -0,0 +1,18 @@
[package]
name = "dialogue_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
smart-default = "0.6.0"
parse-display = "0.1.1"
teloxide = { path = "../../" }
[profile.release]
lto = true

View file

@ -0,0 +1,209 @@
// This is a bot that asks your full name, your age, your favourite kind of
// music and sends all the gathered information back.
//
// # Example
// ```
// - Let's start! First, what's your full name?
// - Luke Skywalker
// - What a wonderful name! Your age?
// - 26
// - Good. Now choose your favourite music
// *A keyboard of music kinds is displayed*
// *You select Metal*
// - Metal
// - Fine. Your full name: Luke Skywalker, your age: 26, your favourite music: Metal
// ```
#![allow(clippy::trivial_regex)]
#[macro_use]
extern crate smart_default;
use teloxide::{
prelude::*,
types::{KeyboardButton, ReplyKeyboardMarkup},
};
use parse_display::{Display, FromStr};
// ============================================================================
// [Favourite music kinds]
// ============================================================================
#[derive(Copy, Clone, Display, FromStr)]
enum FavouriteMusic {
Rock,
Metal,
Pop,
Other,
}
impl FavouriteMusic {
fn markup() -> ReplyKeyboardMarkup {
ReplyKeyboardMarkup::default().append_row(vec![
KeyboardButton::new("Rock"),
KeyboardButton::new("Metal"),
KeyboardButton::new("Pop"),
KeyboardButton::new("Other"),
])
}
}
// ============================================================================
// [A type-safe finite automaton]
// ============================================================================
#[derive(Clone)]
struct ReceiveAgeState {
full_name: String,
}
#[derive(Clone)]
struct ReceiveFavouriteMusicState {
data: ReceiveAgeState,
age: u8,
}
#[derive(Display)]
#[display(
"Your full name: {data.data.full_name}, your age: {data.age}, your \
favourite music: {favourite_music}"
)]
struct ExitState {
data: ReceiveFavouriteMusicState,
favourite_music: FavouriteMusic,
}
#[derive(SmartDefault)]
enum Dialogue {
#[default]
Start,
ReceiveFullName,
ReceiveAge(ReceiveAgeState),
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
}
// ============================================================================
// [Control a dialogue]
// ============================================================================
type Ctx<State> = DialogueHandlerCtx<Message, State>;
type Res = Result<DialogueStage<Dialogue>, RequestError>;
async fn start(ctx: Ctx<()>) -> Res {
ctx.answer("Let's start! First, what's your full name?")
.send()
.await?;
next(Dialogue::ReceiveFullName)
}
async fn full_name(ctx: Ctx<()>) -> Res {
match ctx.update.text() {
None => {
ctx.answer("Please, send me a text message!").send().await?;
next(Dialogue::ReceiveFullName)
}
Some(full_name) => {
ctx.answer("What a wonderful name! Your age?")
.send()
.await?;
next(Dialogue::ReceiveAge(ReceiveAgeState {
full_name: full_name.to_owned(),
}))
}
}
}
async fn age(ctx: Ctx<ReceiveAgeState>) -> Res {
match ctx.update.text().unwrap().parse() {
Ok(age) => {
ctx.answer("Good. Now choose your favourite music:")
.reply_markup(FavouriteMusic::markup())
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(
ReceiveFavouriteMusicState {
data: ctx.dialogue,
age,
},
))
}
Err(_) => {
ctx.answer("Oh, please, enter a number!").send().await?;
next(Dialogue::ReceiveAge(ctx.dialogue))
}
}
}
async fn favourite_music(ctx: Ctx<ReceiveFavouriteMusicState>) -> Res {
match ctx.update.text().unwrap().parse() {
Ok(favourite_music) => {
ctx.answer(format!(
"Fine. {}",
ExitState {
data: ctx.dialogue.clone(),
favourite_music
}
))
.send()
.await?;
exit()
}
Err(_) => {
ctx.answer("Oh, please, enter from the keyboard!")
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(ctx.dialogue))
}
}
}
async fn handle_message(ctx: Ctx<Dialogue>) -> Res {
match ctx {
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::Start,
} => start(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveFullName,
} => full_name(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveAge(s),
} => age(DialogueHandlerCtx::new(bot, update, s)).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveFavouriteMusic(s),
} => favourite_music(DialogueHandlerCtx::new(bot, update, s)).await,
}
}
// ============================================================================
// [Run!]
// ============================================================================
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting dialogue_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
}))
.dispatch()
.await;
}

View file

@ -0,0 +1,15 @@
[package]
name = "guess_a_number_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
smart-default = "0.6.0"
rand = "0.7.3"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -0,0 +1,116 @@
// This is a guess-a-number game!
//
// # Example
// ```
// - Hello
// - Let's play a game! Guess a number from 1 to 10 (inclusively).
// - 4
// - No.
// - 3
// - No.
// - Blablabla
// - Oh, please, send me a text message!
// - 111
// - Oh, please, send me a number in the range [1; 10]!
// - 5
// - Congratulations! You won!
// ```
#[macro_use]
extern crate smart_default;
use teloxide::prelude::*;
use rand::{thread_rng, Rng};
// ============================================================================
// [A type-safe finite automaton]
// ============================================================================
#[derive(SmartDefault)]
enum Dialogue {
#[default]
Start,
ReceiveAttempt(u8),
}
// ============================================================================
// [Control a dialogue]
// ============================================================================
async fn handle_message(
ctx: DialogueHandlerCtx<Message, Dialogue>,
) -> Result<DialogueStage<Dialogue>, RequestError> {
match ctx.dialogue {
Dialogue::Start => {
ctx.answer(
"Let's play a game! Guess a number from 1 to 10 (inclusively).",
)
.send()
.await?;
next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
}
Dialogue::ReceiveAttempt(secret) => match ctx.update.text() {
None => {
ctx.answer("Oh, please, send me a text message!")
.send()
.await?;
next(ctx.dialogue)
}
Some(text) => match text.parse::<u8>() {
Ok(attempt) => match attempt {
x if !(1..=10).contains(&x) => {
ctx.answer(
"Oh, please, send me a number in the range [1; \
10]!",
)
.send()
.await?;
next(ctx.dialogue)
}
x if x == secret => {
ctx.answer("Congratulations! You won!").send().await?;
exit()
}
_ => {
ctx.answer("No.").send().await?;
next(ctx.dialogue)
}
},
Err(_) => {
ctx.answer(
"Oh, please, send me a number in the range [1; 10]!",
)
.send()
.await?;
next(ctx.dialogue)
}
},
},
}
}
// ============================================================================
// [Run!]
// ============================================================================
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting guess_a_number_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
}))
.dispatch()
.await;
}

View file

@ -0,0 +1,13 @@
[package]
name = "multiple_handlers_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -0,0 +1,49 @@
// This example demonstrates the ability of Dispatcher to deal with multiple
// handlers.
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting multiple_handlers_bot!");
let bot = Bot::from_env();
// Create a dispatcher with multiple handlers of different types. This will
// print One! and Two! on every incoming UpdateKind::Message.
Dispatcher::<RequestError>::new(bot)
// This is the first UpdateKind::Message handler, which will be called
// after the Update handler below.
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
log::info!("Two!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
// Remember: handler of Update are called first.
.update_handler(&|ctx: DispatcherHandlerCtx<Update>| async move {
log::info!("One!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
// This handler will be called right after the first UpdateKind::Message
// handler, because it is registered after.
.message_handler(&|_ctx: DispatcherHandlerCtx<Message>| async move {
// The same as DispatcherHandlerResult::exit(Ok(()))
Ok(())
})
// This handler will never be called, because the UpdateKind::Message
// handler above terminates the pipeline.
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
log::info!("This will never be printed!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
.dispatch()
.await;
// Note: if this bot receive, for example, UpdateKind::ChannelPost, it will
// only print "One!", because the UpdateKind::Message handlers will not be
// called.
}

View file

@ -0,0 +1,16 @@
[package]
name = "ping_pong_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }
[profile.release]
lto = true

View file

@ -0,0 +1,23 @@
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting ping_pong_bot!");
let bot = Bot::from_env();
// Create a dispatcher with a single message handler that answers "pong" to
// each incoming message.
Dispatcher::<RequestError>::new(bot)
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
ctx.answer("pong").send().await?;
Ok(())
})
.dispatch()
.await;
}

View file

@ -0,0 +1,14 @@
[package]
name = "simple_commands_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
rand = "0.7.3"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -0,0 +1,63 @@
use teloxide::{prelude::*, utils::command::BotCommand};
use rand::{thread_rng, Rng};
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "display this text.")]
Help,
#[command(description = "be a cat.")]
Meow,
#[command(description = "generate a random number within [0; 1).")]
Generate,
}
async fn handle_command(
ctx: DispatcherHandlerCtx<Message>,
) -> Result<(), RequestError> {
let text = match ctx.update.text() {
Some(text) => text,
None => {
log::info!("Received a message, but not text.");
return Ok(());
}
};
let command = match Command::parse(text) {
Some((command, _)) => command,
None => {
log::info!("Received a text message, but not a command.");
return Ok(());
}
};
match command {
Command::Help => ctx.answer(Command::descriptions()).send().await?,
Command::Generate => {
ctx.answer(thread_rng().gen_range(0.0, 1.0).to_string())
.send()
.await?
}
Command::Meow => ctx.answer("I am a cat! Meow!").send().await?,
};
Ok(())
}
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting simple_commands_bot!");
let bot = Bot::from_env();
Dispatcher::<RequestError>::new(bot)
.message_handler(&handle_command)
.dispatch()
.await;
}

405
logo.svg Normal file
View file

@ -0,0 +1,405 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 1000.1765 1000.1688"
height="1000.1688mm"
width="1000.1688mm">
<defs
id="defs2">
<radialGradient
id="radialGradient2098"
cx="257.63312"
cy="346.10947"
r="1837.1556"
fx="323.34329"
fy="313.63162"
gradientTransform="matrix(0.25369962,0,0,0.25369962,-147.14212,-160.64302)"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#FF980E"
id="stop2086" />
<stop
offset="0.295012"
style="stop-color:#FF7139"
id="stop2088" />
<stop
offset="0.4846462"
style="stop-color:#FF5B51"
id="stop2090" />
<stop
offset="0.6260016"
style="stop-color:#FF4F5E"
id="stop2092" />
<stop
offset="0.73652"
style="stop-color:#f6374e;stop-opacity:0.97254902"
id="stop2094" />
<stop
offset="0.8428285"
style="stop-color:#f52d44;stop-opacity:1"
id="stop2096" />
</radialGradient>
<radialGradient
id="radialGradient7778"
cx="624.28052"
cy="138.58418"
r="3105.1294"
gradientTransform="matrix(0.9588647 0 0 0.9588647 1293.9r906006 17.7235451)"
gradientUnits="userSpaceOnUse">
<stop
offset="0.0535657"
style="stop-color:#FFF44F"
id="stop7768" />
<stop
offset="0.4572717"
style="stop-color:#FF980E"
id="stop7770" />
<stop
offset="0.5210502"
style="stop-color:#FF8424"
id="stop7772" />
<stop
offset="0.5831793"
style="stop-color:#FF7634"
id="stop7774" />
<stop
offset="0.639343"
style="stop-color:#FF7139"
id="stop7776" />
</radialGradient>
<linearGradient
y2="180"
x2="100.008"
y1="40.007999"
x1="160.008"
id="linearGradient7176"
gradientUnits="userSpaceOnUse">
<stop
id="stop7172"
offset="0"
stop-color="#37aee2"
style="stop-color:#ff3750;stop-opacity:0" />
<stop
id="stop7174"
offset="1"
stop-color="#1e96c8"
style="stop-color:#ffd865;stop-opacity:1" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id="b"
x1="160.008"
y1="40.007999"
x2="100.008"
y2="180">
<stop
style="stop-color:#37aee2;stop-opacity:0"
stop-color="#37aee2"
offset="0"
id="stop4892" />
<stop
style="stop-color:#1e96c8;stop-opacity:0"
stop-color="#1e96c8"
offset="1"
id="stop4894" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.0919081,0.91582798)"
id="w"
x1="140.86748"
y1="147.14627"
x2="88.524704"
y2="112.05341">
<stop
style="stop-color:#a047bf;stop-opacity:1"
stop-color="#eff7fc"
offset="0"
id="stop4897" />
<stop
style="stop-color:#ee7e40;stop-opacity:1"
stop-color="#fff"
offset="1"
id="stop4899" />
</linearGradient>
<linearGradient
y2="180"
x2="100.008"
y1="40.007999"
x1="160.008"
gradientUnits="userSpaceOnUse"
id="linearGradient7166"
xlink:href="#b" />
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.25369962,0,0,0.25369962,116.95639,-406.02996)"
r="3105.1294"
cy="138.58418"
cx="624.28052"
id="SVGID_11_">
<stop
id="stop135"
style="stop-color:#FFF44F"
offset="0.0535657" />
<stop
id="stop137"
style="stop-color:#FF980E"
offset="0.4572717" />
<stop
id="stop139"
style="stop-color:#FF8424"
offset="0.5210502" />
<stop
id="stop141"
style="stop-color:#FF7634"
offset="0.587052" />
<stop
id="stop143"
style="stop-color:#FF7139"
offset="0.639343" />
</radialGradient>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4.4292543,-0.00937413,0.00863853,4.0816693,-654.99926,-176.9423)"
r="65.246368"
fy="60.625607"
fx="198.56102"
cy="60.625607"
cx="198.56102"
id="radialGradient1819"
xlink:href="#SVGID_7_" />
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.25369962,0,0,0.25369962,-147.14212,-160.64302)"
fy="313.63162"
fx="323.34329"
r="1837.1556"
cy="346.10947"
cx="257.63312"
id="SVGID_7_">
<stop
id="stop85"
style="stop-color:#FF980E"
offset="0" />
<stop
id="stop87"
style="stop-color:#FF7139"
offset="0.295012" />
<stop
id="stop89"
style="stop-color:#FF5B51"
offset="0.4846462" />
<stop
id="stop91"
style="stop-color:#FF4F5E"
offset="0.6260016" />
<stop
id="stop93"
style="stop-color:#FF4055"
offset="0.73652" />
<stop
id="stop95"
style="stop-color:#FF3750"
offset="0.8428285" />
</radialGradient>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.26458333,0,0,0.26458333,-232.53772,-527.76884)"
fy="-153.06329"
fx="389.09302"
r="1876.7874"
cy="-119.88482"
cx="321.9653"
id="SVGID_1_">
<stop
id="stop3"
style="stop-color:#FFF44F"
offset="0" />
<stop
id="stop5"
style="stop-color:#FF980E"
offset="0.2948518" />
<stop
id="stop7"
style="stop-color:#FF5D36"
offset="0.4315208" />
<stop
id="stop9"
style="stop-color:#FF3750"
offset="0.5302083" />
<stop
id="stop11"
style="stop-color:#F5156C"
offset="0.7493189" />
<stop
id="stop13"
style="stop-color:#F1136E"
offset="0.7647903" />
<stop
id="stop15"
style="stop-color:#DA057A"
offset="0.8800957" />
<stop
id="stop17"
style="stop-color:#D2007F"
offset="0.9527844" />
</radialGradient>
<linearGradient
gradientTransform="matrix(0.49700557,0,0,0.49700557,7.2354759,-1184.0654)"
y2="1708.0002"
x2="477.68073"
y1="246.00212"
x1="1321.7657"
gradientUnits="userSpaceOnUse"
id="SVGID_12_">
<stop
id="stop2743"
style="stop-color:#FFF44F;stop-opacity:0.8"
offset="0" />
<stop
id="stop2745"
style="stop-color:#FFF44F;stop-opacity:0"
offset="0.75" />
</linearGradient>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.25369962,0,0,0.25369962,-147.14212,-160.64302)"
fy="313.63162"
fx="323.34329"
r="1837.1556"
cy="346.10947"
cx="257.63312"
id="SVGID_7_-2">
<stop
id="stop85-9"
style="stop-color:#FF980E"
offset="0" />
<stop
id="stop87-1"
style="stop-color:#FF7139"
offset="0.295012" />
<stop
id="stop89-2"
style="stop-color:#FF5B51"
offset="0.4846462" />
<stop
id="stop91-7"
style="stop-color:#FF4F5E"
offset="0.6260016" />
<stop
id="stop93-0"
style="stop-color:#FF4055"
offset="0.73652" />
<stop
id="stop95-9"
style="stop-color:#FF3750"
offset="0.8428285" />
</radialGradient>
<linearGradient
gradientTransform="translate(-290.96154,-5.268689)"
gradientUnits="userSpaceOnUse"
y2="220.18013"
x2="24.136377"
y1="-7.9157548"
x1="238.16116"
id="linearGradient1127"
xlink:href="#SVGID_12_" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="display:inline"
transform="translate(-25.485081,874.03927)"
id="layer1">
<rect
ry="122.74884"
y="-873.87427"
x="25.485081"
height="1000.0077"
width="1000.0077"
id="rect99"
style="fill:#282828;fill-opacity:1;fill-rule:evenodd;stroke-width:0.24541904" />
<g
transform="matrix(3.4097882,0,0,3.4097882,-58.261564,-771.89541)"
id="g4557">
<path
transform="scale(0.26458333)"
style="fill:#f1f1f1;fill-opacity:1;stroke:#f1f1f1;stroke-width:10.54206085;stroke-opacity:1"
d="m 647.14258,-57.232422 c -274.72614,0 -498.32227,223.703512 -498.32227,498.324222 0,274.6207 223.70155,498.32226 498.32227,498.32226 274.6207,0 498.32422,-223.70156 498.32422,-498.32226 0,-274.62071 -223.70352,-498.324222 -498.32422,-498.324222 z m -0.74219,44.277344 a 32.785812,32.785812 0 0 1 0.004,0 32.680391,32.785812 0 0 1 31.83789,32.785156 32.786135,32.786135 0 0 1 -65.57227,0 32.785812,32.785812 0 0 1 33.73047,-32.785156 z m -74.10547,53.974609 50.4961,52.921875 c 11.38541,11.912534 30.25542,12.440064 42.16796,0.949219 l 56.50586,-53.871094 a 403.44469,403.44469 0 0 1 66.1543,18.777344 c 1.97576,0.407587 3.93673,0.970989 5.87695,1.730469 13.99934,5.479892 27.61358,11.724081 40.80079,18.671875 a 403.44469,403.44469 0 0 1 2.36523,1.269531 c 10.36827,5.551273 20.46754,11.537743 30.26953,17.943359 a 403.44469,403.44469 0 0 1 6.39844,4.269531 c 8.32464,5.66907 16.42445,11.64272 24.28711,17.90234 a 403.44469,403.44469 0 0 1 9.8125,8.05664 c 6.45209,5.47512 12.72445,11.15359 18.81836,17.01758 a 403.44469,403.44469 0 0 1 11.94922,11.9668 c 5.13237,5.3678 10.11852,10.87734 14.95117,16.52148 a 403.44469,403.44469 0 0 1 12.51172,15.37696 c 3.37107,4.35061 6.66483,8.76435 9.85937,13.2539 0.23859,0.33532 0.45279,0.6794 0.67383,1.02149 a 403.44469,403.44469 0 0 1 21.47266,32.93555 l -38.68946,87.28906 c -6.6415,15.07515 0.21195,32.78628 15.18164,39.5332 l 74.4258,32.99609 a 403.44469,403.44469 0 0 1 0.8437,69.99805 h -0.5 c -5.7455,78.10094 -33.7008,150.02515 -77.59567,209.47266 h 0.7168 a 403.44469,403.44469 0 0 1 -37.31836,43.2207 l -69.26172,-14.86328 c -16.12936,-3.47887 -32.04848,6.85115 -35.52734,22.98047 l -16.44532,76.74805 a 403.44469,403.44469 0 0 1 -36.45312,14.36328 c -0.0902,0.0369 -0.18261,0.0712 -0.27344,0.10742 a 403.44469,403.44469 0 0 1 -1.01367,0.375 c -0.24074,0.087 -0.47392,0.18338 -0.71875,0.26562 -40.39438,13.56934 -83.64335,20.92188 -128.60938,20.92188 -40.73374,0 -80.05693,-6.03481 -117.12695,-17.25781 -2.64112,-0.79961 -5.08556,-1.88567 -7.35742,-3.22071 A 403.44469,403.44469 0 0 1 477.09961,803.52734 L 460.6543,726.78125 c -3.47891,-16.12936 -19.29252,-26.46129 -35.42188,-22.98242 l -67.78515,14.54883 a 403.44469,403.44469 0 0 1 -21.21289,-24.19141 c -1.98,-1.33851 -3.76227,-2.94462 -5.30469,-4.875 -55.15082,-69.02256 -88.12305,-156.54162 -88.12305,-251.76172 0,-2.92892 0.34165,-5.66443 0.98047,-8.23242 a 403.44469,403.44469 0 0 1 2.23047,-35.21289 l 70.63086,-31.41602 c 15.07515,-6.74692 21.92856,-24.35258 15.18164,-39.42773 l -14.54883,-32.78516 h 0.26563 c -13.3252,-23.74604 -27.38086,-44.84375 -27.38086,-44.84375 10.41967,-18.87574 21.81773,-35.1234 34.7871,-52.17773 51.04777,-67.12628 122.64108,-117.779826 205.4043,-142.587892 2.17055,-0.650615 4.32668,-0.947976 6.47266,-0.953126 a 403.44469,403.44469 0 0 1 35.46484,-8.863281 z M 255.5,283.69727 a 32.785812,32.785812 0 0 1 0.006,0 32.785812,32.785812 0 0 1 31.83594,32.78711 32.785812,32.785812 0 0 1 -65.57032,0 A 32.785812,32.785812 0 0 1 255.5,283.69727 Z m 781.6934,1.47656 a 32.785812,32.785812 0 0 1 0.01,0 32.785812,32.785812 0 0 1 31.8379,32.78515 32.78615,32.78615 0 0 1 -65.5723,0 32.785812,32.785812 0 0 1 33.7286,-32.78515 z M 403.61523,745.33398 a 32.785812,32.785812 0 0 1 0.006,0.002 32.785812,32.785812 0 0 1 31.83789,32.78515 32.78613,32.78613 0 0 1 -65.57226,0 32.785812,32.785812 0 0 1 33.72851,-32.78711 z m 485.46289,1.47657 a 32.785812,32.785812 0 0 1 0.006,0 32.785812,32.785812 0 0 1 31.83594,32.78711 32.78613,32.78613 0 0 1 -65.57226,0 32.785812,32.785812 0 0 1 33.73046,-32.78711 z"
id="path4504" />
<path
style="fill:#f1f1f1;fill-opacity:1;fill-rule:evenodd;stroke:#f1f1f1;stroke-width:8.36776161;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 295.70761,116.70536 A 124.4844,124.4844 0 0 1 171.22322,241.18976 124.4844,124.4844 0 0 1 46.738819,116.70536 124.4844,124.4844 0 0 1 171.22322,-7.779039 124.4844,124.4844 0 0 1 295.70761,116.70536 Z m -2.34297,-12.02169 19.41321,12.02169 -19.41321,12.02168 16.67974,15.59193 -21.36569,8.00516 13.33264,18.54854 -22.56507,3.68181 9.48347,20.80784 -22.84399,-0.80889 5.2438,22.25825 -22.25825,-5.2438 0.80888,22.84399 -20.80783,-9.48346 -3.68181,22.56506 -18.54854,-13.33263 -8.00516,21.36568 -15.59193,-16.67974 -12.02168,19.41321 -12.02169,-19.41321 -15.59193,16.67974 -8.00515,-21.36568 -18.54854,13.33263 -3.68182,-22.56506 -20.807831,9.48346 0.808884,-22.84399 -22.258245,5.2438 5.243797,-22.25825 -22.843989,0.80889 9.483463,-20.80784 L 40.435106,170.87267 53.767739,152.32413 32.402055,144.31897 49.081793,128.72704 29.668586,116.70536 49.081793,104.68367 32.402055,89.091745 53.767739,81.086587 40.435106,62.538049 63.000169,58.856234 53.516706,38.048401 l 22.843989,0.808884 -5.243797,-22.258246 22.258245,5.243797 -0.808884,-22.8439882 20.807831,9.4834629 3.68182,-22.5650637 18.54854,13.33263359 8.00515,-21.36568459 15.59193,16.6797382 12.02169,-19.4132062 12.02168,19.4132062 15.59193,-16.6797382 8.00516,21.36568459 18.54854,-13.33263359 3.68181,22.5650637 20.80783,-9.4834629 -0.80888,22.8439882 22.25825,-5.243797 -5.2438,22.258246 22.84399,-0.808884 -9.48347,20.807833 22.56507,3.681815 -13.33264,18.548538 21.36569,8.005158 z"
id="path4506" />
</g>
<g
transform="matrix(2.9858713,0,0,2.9858713,167.26874,-732.2594)"
id="g4928">
<circle
style="fill:url(#linearGradient7166);fill-opacity:1"
cx="120"
cy="120"
r="120"
id="circle4904" />
<path
style="fill:#bf2543;fill-opacity:1"
d="m 98,175 c -3.8876,0 -3.227,-1.4679 -4.5678,-5.1695 L 82,132.2059 170,80"
id="path4906" />
<path
style="fill:#a92543;fill-opacity:1"
d="m 98,175 c 3,0 4.3255,-1.372 6,-3 l 16,-15.558 -19.958,-12.035"
id="path4908" />
<path
style="fill:url(#radialGradient1819);fill-opacity:1"
d="m 100.04,144.41 48.36,35.729 c 5.5185,3.0449 9.5014,1.4684 10.876,-5.1235 l 19.685,-92.763 c 2.0154,-8.0802 -3.0801,-11.745 -8.3594,-9.3482 l -115.59,44.571 c -7.8901,3.1647 -7.8441,7.5666 -1.4382,9.528 l 29.663,9.2583 68.673,-43.325 c 3.2419,-1.9659 6.2173,-0.90899 3.7752,1.2584"
id="path4910" />
</g>
<rect
y="-44.385658"
x="34.240719"
height="159.17772"
width="164.35423"
id="rect2241"
style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke-width:0.25622422" />
<g
transform="matrix(0.53235487,0,0,0.53235487,-27.283341,10.578064)"
id="g4557-3"
style="display:inline">
<path
style="fill:#282828;fill-opacity:1;stroke:#282828;stroke-width:2.78925347;stroke-opacity:1"
d="m -136.20958,-18.218258 c -72.68796,0 -131.84776,59.18822 -131.84776,131.848288 0,72.66006 59.1877,131.84776 131.84776,131.84776 72.660061,0 131.8482833,-59.1877 131.8482833,-131.84776 0,-72.660068 -59.1882223,-131.848288 -131.8482833,-131.848288 z m -0.19637,11.7150471 a 8.6745793,8.6745793 0 0 1 10e-4,0 8.6466867,8.6745793 0 0 1 8.42377,8.6744058 8.674665,8.674665 0 0 1 -17.34933,0 8.6745793,8.6745793 0 0 1 8.92452,-8.6744058 z m -19.60707,14.2807817 13.36042,14.0022462 c 3.01239,3.151858 8.00508,3.291434 11.15694,0.251148 l 14.95051,-14.2533942 a 106.74474,106.74474 0 0 1 17.503328,4.9681722 c 0.522753,0.107841 1.041593,0.256908 1.554943,0.457854 3.703992,1.449888 7.306093,3.101996 10.795209,4.940266 a 106.74474,106.74474 0 0 1 0.6258,0.335897 c 2.743271,1.468774 5.41537,3.052695 8.008813,4.747514 a 106.74474,106.74474 0 0 1 1.692921,1.129647 c 2.202561,1.499941 4.345635,3.080469 6.425964,4.73666 a 106.74474,106.74474 0 0 1 2.596224,2.131653 c 1.707115,1.448625 3.366677,2.951054 4.979024,4.502568 a 106.74474,106.74474 0 0 1 3.161565,3.166216 c 1.357939,1.42023 2.677192,2.877963 3.95583,4.371308 a 106.74474,106.74474 0 0 1 3.310393,4.068487 c 0.891929,1.151099 1.763403,2.318901 2.608625,3.506761 0.06313,0.08872 0.1198,0.179758 0.178284,0.270269 a 106.74474,106.74474 0 0 1 5.681308,8.714198 l -10.236586,23.09523 c -1.757231,3.988633 0.05608,8.674703 4.016808,10.459826 l 19.691826,8.730223 a 106.74474,106.74474 0 0 1 0.223229,18.52031 h -0.132291 c -1.520164,20.66421 -8.91667,39.69416 -20.530521,55.42298 h 0.189653 a 106.74474,106.74474 0 0 1 -9.873816,11.43547 l -18.325496,-3.93257 c -4.26756,-0.92045 -8.479494,1.8127 -9.399942,6.08025 l -4.351158,20.30625 a 106.74474,106.74474 0 0 1 -9.644887,3.80029 c -0.0239,0.01 -0.0483,0.0188 -0.0724,0.0284 a 106.74474,106.74474 0 0 1 -0.2682,0.0992 c -0.0637,0.023 -0.12539,0.0485 -0.19017,0.0703 -10.68768,3.59023 -22.13063,5.53558 -34.0279,5.53558 -10.77746,0 -21.18172,-1.59671 -30.98983,-4.56612 -0.6988,-0.21157 -1.34556,-0.49892 -1.94665,-0.85215 a 106.74474,106.74474 0 0 1 -11.86491,-4.53409 l -4.35116,-20.30574 c -0.92046,-4.26756 -5.10448,-7.00121 -9.37204,-6.08076 l -17.93482,3.84938 a 106.74474,106.74474 0 0 1 -5.61258,-6.40065 c -0.52387,-0.35415 -0.99543,-0.7791 -1.40353,-1.28984 -14.59199,-18.26222 -23.31589,-41.4183 -23.31589,-66.61196 0,-0.77494 0.0904,-1.49871 0.25942,-2.17816 a 106.74474,106.74474 0 0 1 0.59014,-9.31674 l 18.68775,-8.312159 c 3.98863,-1.785122 5.80193,-6.443287 4.01681,-10.43192 l -3.84938,-8.674407 h 0.0703 c -3.52562,-6.282806 -7.24452,-11.864908 -7.24452,-11.864908 2.75688,-4.994207 5.77261,-9.293067 9.20409,-13.805358 13.50639,-17.760495 32.44879,-31.162579 54.34656,-37.726379 0.57429,-0.172142 1.14476,-0.250819 1.71255,-0.252181 a 106.74474,106.74474 0 0 1 9.38341,-2.3450772 z m -83.81866,64.2084842 a 8.6745793,8.6745793 0 0 1 0.002,0 8.6745793,8.6745793 0 0 1 8.42326,8.674923 8.6745793,8.6745793 0 0 1 -17.34881,0 8.6745793,8.6745793 0 0 1 8.92396,-8.674923 z m 206.823047,0.390674 a 8.6745793,8.6745793 0 0 1 0.0026,0 8.6745793,8.6745793 0 0 1 8.423778,8.674404 8.674669,8.674669 0 0 1 -17.349338,0 8.6745793,8.6745793 0 0 1 8.924026,-8.674404 z M -200.64286,194.12744 a 8.6745793,8.6745793 0 0 1 0.002,5.3e-4 8.6745793,8.6745793 0 0 1 8.42378,8.6744 8.674665,8.674665 0 0 1 -17.34933,0 8.6745793,8.6745793 0 0 1 8.924,-8.67492 z m 128.445393,0.39067 a 8.6745793,8.6745793 0 0 1 0.0016,0 8.6745793,8.6745793 0 0 1 8.423259,8.67492 8.6746635,8.6746635 0 0 1 -17.349327,0 8.6745793,8.6745793 0 0 1 8.924518,-8.67492 z"
id="path4504-6" />
<path
style="fill:#282828;fill-opacity:1;fill-rule:evenodd;stroke:#282828;stroke-width:8.36776161;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m -11.724928,113.62977 a 124.4844,124.4844 0 0 1 -124.484392,124.4844 124.4844,124.4844 0 0 1 -124.4844,-124.4844 124.4844,124.4844 0 0 1 124.4844,-124.484403 124.4844,124.4844 0 0 1 124.484392,124.484403 z m -2.34297,-12.02169 19.4132097,12.02169 -19.4132097,12.02168 16.6797397,15.59193 -21.3656897,8.00516 13.3326397,18.54854 -22.5650697,3.68181 9.48347,20.80784 -22.84399,-0.80889 5.2438,22.25825 -22.25825,-5.2438 0.80888,22.84399 -20.80783,-9.48346 -3.68181,22.56506 -18.548542,-13.33263 -8.00516,21.36568 -15.59193,-16.67974 -12.02168,19.41321 -12.02169,-19.41321 -15.59193,16.67974 -8.00515,-21.36568 -18.54854,13.33263 -3.68182,-22.56506 -20.80783,9.48346 0.80888,-22.84399 -22.25824,5.2438 5.2438,-22.25825 -22.84399,0.80889 9.48346,-20.80784 -22.56506,-3.68181 13.33263,-18.54854 -21.36568,-8.00516 16.67973,-15.59193 -19.4132,-12.02168 19.4132,-12.02169 -16.67973,-15.591929 21.36568,-8.005158 -13.33263,-18.548538 22.56506,-3.681815 -9.48346,-20.807833 22.84399,0.808884 -5.2438,-22.258246 22.25824,5.243797 -0.80888,-22.8439878 20.80783,9.4834629 3.68182,-22.5650641 18.54854,13.332634 8.00515,-21.365685 15.59193,16.6797386 12.02169,-19.4132066 12.02168,19.4132066 15.59193,-16.6797386 8.00516,21.365685 18.548542,-13.332634 3.68181,22.5650641 20.80783,-9.4834629 -0.80888,22.8439878 22.25825,-5.243797 -5.2438,22.258246 22.84399,-0.808884 -9.48347,20.807833 22.5650697,3.681815 -13.3326397,18.548538 21.3656897,8.005158 z"
id="path4506-7" />
</g>
<g
transform="matrix(2.9858713,0,0,2.9858713,1047.9069,-718.22402)"
id="g4928-3"
style="display:inline">
<path
id="path4906-0"
d="m -121.42796,67.483064 c -0.93128,0.03404 -1.91559,0.271144 -2.90546,0.720544 l -115.58979,44.570682 c -7.8901,3.1647 -7.84366,7.56642 -1.43777,9.52782 l 29.65763,9.25734 10.20069,33.56963 c 1.34081,3.70159 0.68066,5.17018 4.56826,5.17018 3,0 4.32487,-1.37279 5.99937,-3.00079 l 14.41311,-14.01404 29.98686,22.15398 c 5.51849,3.0449 9.5023,1.46827 10.87689,-5.12363 l 19.68418,-92.762936 c 1.63751,-6.565163 -1.41841,-10.216308 -5.45397,-10.06878 z"
style="fill:url(#linearGradient1127);fill-opacity:1;stroke-width:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

5
rustfmt.toml Normal file
View file

@ -0,0 +1,5 @@
format_code_in_doc_comments = true
wrap_comments = true
format_strings = true
max_width = 80
merge_imports = true

1590
src/bot/api.rs Normal file

File diff suppressed because it is too large Load diff

66
src/bot/download.rs Normal file
View file

@ -0,0 +1,66 @@
use tokio::io::AsyncWrite;
#[cfg(feature = "unstable-stream")]
use ::{bytes::Bytes, tokio::stream::Stream};
#[cfg(feature = "unstable-stream")]
use crate::net::download_file_stream;
use crate::{bot::Bot, net::download_file, DownloadError};
impl Bot {
/// Download a file from Telegram into `destination`.
///
/// `path` can be obtained from [`Bot::get_file`].
///
/// To download as a stream of chunks, see [`Bot::download_file_stream`].
///
/// ## Examples
///
/// ```no_run
/// use teloxide::types::File as TgFile;
/// use tokio::fs::File;
/// # use teloxide::RequestError;
/// use teloxide::{requests::Request, Bot};
///
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let bot = Bot::new("TOKEN");
/// let mut file = File::create("/home/waffle/Pictures/test.png").await?;
///
/// let TgFile { file_path, .. } = bot.get_file("*file_id*").send().await?;
/// bot.download_file(&file_path, &mut file).await?;
/// # Ok(()) }
/// ```
///
/// [`Bot::get_file`]: crate::Bot::get_file
/// [`Bot::download_file_stream`]: crate::Bot::download_file_stream
pub async fn download_file<D>(
&self,
path: &str,
destination: &mut D,
) -> Result<(), DownloadError>
where
D: AsyncWrite + Unpin,
{
download_file(&self.client, &self.token, path, destination).await
}
/// Download a file from Telegram.
///
/// `path` can be obtained from the [`Bot::get_file`].
///
/// To download into [`AsyncWrite`] (e.g. [`tokio::fs::File`]), see
/// [`Bot::download_file`].
///
/// [`Bot::get_file`]: crate::bot::Bot::get_file
/// [`AsyncWrite`]: tokio::io::AsyncWrite
/// [`tokio::fs::File`]: tokio::fs::File
/// [`Bot::download_file`]: crate::Bot::download_file
#[cfg(feature = "unstable-stream")]
pub async fn download_file_stream(
&self,
path: &str,
) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>>, reqwest::Error>
{
download_file_stream(&self.client, &self.token, path).await
}
}

80
src/bot/mod.rs Normal file
View file

@ -0,0 +1,80 @@
use reqwest::Client;
use std::sync::Arc;
mod api;
mod download;
/// A Telegram bot used to send requests.
#[derive(Debug, Clone)]
pub struct Bot {
token: String,
client: Client,
}
impl Bot {
/// Creates a new `Bot` with the `TELOXIDE_TOKEN` environmental variable (a
/// bot's token) and the default [`reqwest::Client`].
///
/// # Panics
/// If cannot get the `TELOXIDE_TOKEN` environmental variable.
///
/// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
pub fn from_env() -> Arc<Self> {
Self::new(
std::env::var("TELOXIDE_TOKEN")
.expect("Cannot get the TELOXIDE_TOKEN env variable"),
)
}
/// Creates a new `Bot` with the `TELOXIDE_TOKEN` environmental variable (a
/// bot's token) and your [`reqwest::Client`].
///
/// # Panics
/// If cannot get the `TELOXIDE_TOKEN` environmental variable.
///
/// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
pub fn from_env_with_client(client: Client) -> Arc<Self> {
Self::with_client(
std::env::var("TELOXIDE_TOKEN")
.expect("Cannot get the TELOXIDE_TOKEN env variable"),
client,
)
}
/// Creates a new `Bot` with the specified token and the default
/// [`reqwest::Client`].
///
/// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
pub fn new<S>(token: S) -> Arc<Self>
where
S: Into<String>,
{
Self::with_client(token, Client::new())
}
/// Creates a new `Bot` with the specified token and your
/// [`reqwest::Client`].
///
/// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
pub fn with_client<S>(token: S, client: Client) -> Arc<Self>
where
S: Into<String>,
{
Arc::new(Self {
token: token.into(),
client,
})
}
}
impl Bot {
// TODO: const fn
pub fn token(&self) -> &str {
&self.token
}
// TODO: const fn
pub fn client(&self) -> &Client {
&self.client
}
}

View file

@ -1,64 +0,0 @@
use futures::compat::Future01CompatExt;
use reqwest::r#async::Client;
use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::Value;
lazy_static! {
static ref REQWEST_CLIENT: Client = Client::new();
}
const TELEGRAM_URL_START: &str = "https://api.telegram.org/bot";
#[derive(Debug)]
pub enum Error {
Api {
status_code: StatusCode,
description: Option<String>,
},
Send(reqwest::Error),
InvalidJson(reqwest::Error),
}
pub type Response<T> = Result<T, Error>;
#[derive(Debug, Deserialize)]
pub struct User {
id: i64,
is_bot: bool,
first_name: String,
last_name: Option<String>,
username: Option<String>,
language_code: Option<String>,
}
pub async fn get_me(bot_token: &str) -> Response<User> {
let mut response = REQWEST_CLIENT
.get(&format!(
"{}{bot_token}/getMe",
TELEGRAM_URL_START,
bot_token = bot_token
))
.send()
.compat()
.await
.map_err(Error::Send)?;
let response_json = response
.json::<Value>()
.compat()
.await
.map_err(Error::InvalidJson)?;
if response_json["ok"] == "false" {
return Err(Error::Api {
status_code: response.status(),
description: match response_json.get("description") {
None => None,
Some(description) => Some(description.to_string()),
},
});
}
Ok(serde_json::from_value(response_json["result"].clone()).unwrap())
}

View file

@ -0,0 +1,31 @@
use std::{future::Future, pin::Pin};
/// An asynchronous handler of a context.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching).
pub trait CtxHandler<Ctx, Output> {
#[must_use]
fn handle_ctx<'a>(
&'a self,
ctx: Ctx,
) -> Pin<Box<dyn Future<Output = Output> + 'a>>
where
Ctx: 'a;
}
impl<Ctx, Output, F, Fut> CtxHandler<Ctx, Output> for F
where
F: Fn(Ctx) -> Fut,
Fut: Future<Output = Output>,
{
fn handle_ctx<'a>(
&'a self,
ctx: Ctx,
) -> Pin<Box<dyn Future<Output = Fut::Output> + 'a>>
where
Ctx: 'a,
{
Box::pin(async move { self(ctx).await })
}
}

View file

@ -0,0 +1,97 @@
use crate::dispatching::{
dialogue::{
DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage, Storage,
},
CtxHandler, DispatcherHandlerCtx,
};
use std::{future::Future, pin::Pin};
/// A dispatcher of dialogues.
///
/// Note that `DialogueDispatcher` implements `CtxHandler`, so you can just put
/// an instance of this dispatcher into the [`Dispatcher`]'s methods.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub struct DialogueDispatcher<'a, D, H> {
storage: Box<dyn Storage<D> + 'a>,
handler: H,
}
impl<'a, D, H> DialogueDispatcher<'a, D, H>
where
D: Default + 'a,
{
/// Creates a dispatcher with the specified `handler` and [`InMemStorage`]
/// (a default storage).
///
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[must_use]
pub fn new(handler: H) -> Self {
Self {
storage: Box::new(InMemStorage::default()),
handler,
}
}
/// Creates a dispatcher with the specified `handler` and `storage`.
#[must_use]
pub fn with_storage<Stg>(handler: H, storage: Stg) -> Self
where
Stg: Storage<D> + 'a,
{
Self {
storage: Box::new(storage),
handler,
}
}
}
impl<'a, D, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for DialogueDispatcher<'a, D, H>
where
H: CtxHandler<DialogueHandlerCtx<Upd, D>, DialogueStage<D>>,
Upd: GetChatId,
D: Default,
{
fn handle_ctx<'b>(
&'b self,
ctx: DispatcherHandlerCtx<Upd>,
) -> Pin<Box<dyn Future<Output = Result<(), ()>> + 'b>>
where
Upd: 'b,
{
Box::pin(async move {
let chat_id = ctx.update.chat_id();
let dialogue = self
.storage
.remove_dialogue(chat_id)
.await
.unwrap_or_default();
if let DialogueStage::Next(new_dialogue) = self
.handler
.handle_ctx(DialogueHandlerCtx {
bot: ctx.bot,
update: ctx.update,
dialogue,
})
.await
{
if self
.storage
.update_dialogue(chat_id, new_dialogue)
.await
.is_some()
{
panic!(
"We previously storage.remove_dialogue() so \
storage.update_dialogue() must return None"
);
}
}
Ok(())
})
}
}

View file

@ -0,0 +1,190 @@
use crate::{
dispatching::dialogue::GetChatId,
requests::{
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
SendVenue, SendVideo, SendVideoNote, SendVoice,
},
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
Bot,
};
use std::sync::Arc;
/// A context of a [`DialogueDispatcher`]'s message handler.
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
pub struct DialogueHandlerCtx<Upd, D> {
pub bot: Arc<Bot>,
pub update: Upd,
pub dialogue: D,
}
impl<Upd, D> DialogueHandlerCtx<Upd, D> {
/// Creates a new instance with the provided fields.
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
Self {
bot,
update,
dialogue,
}
}
/// Creates a new instance by substituting a dialogue and preserving
/// `self.bot` and `self.update`.
pub fn with_new_dialogue<Nd>(
self,
new_dialogue: Nd,
) -> DialogueHandlerCtx<Upd, Nd> {
DialogueHandlerCtx {
bot: self.bot,
update: self.update,
dialogue: new_dialogue,
}
}
}
impl<Upd, D> GetChatId for DialogueHandlerCtx<Upd, D>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.update.chat_id()
}
}
impl<D> DialogueHandlerCtx<Message, D> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot.send_message(self.chat_id(), text)
}
pub fn reply_to<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot
.send_message(self.chat_id(), text)
.reply_to_message_id(self.update.id)
}
pub fn answer_photo(&self, photo: InputFile) -> SendPhoto {
self.bot.send_photo(self.update.chat.id, photo)
}
pub fn answer_audio(&self, audio: InputFile) -> SendAudio {
self.bot.send_audio(self.update.chat.id, audio)
}
pub fn answer_animation(&self, animation: InputFile) -> SendAnimation {
self.bot.send_animation(self.update.chat.id, animation)
}
pub fn answer_document(&self, document: InputFile) -> SendDocument {
self.bot.send_document(self.update.chat.id, document)
}
pub fn answer_video(&self, video: InputFile) -> SendVideo {
self.bot.send_video(self.update.chat.id, video)
}
pub fn answer_voice(&self, voice: InputFile) -> SendVoice {
self.bot.send_voice(self.update.chat.id, voice)
}
pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup
where
T: Into<Vec<InputMedia>>,
{
self.bot.send_media_group(self.update.chat.id, media_group)
}
pub fn answer_location(
&self,
latitude: f32,
longitude: f32,
) -> SendLocation {
self.bot
.send_location(self.update.chat.id, latitude, longitude)
}
pub fn answer_venue<T, U>(
&self,
latitude: f32,
longitude: f32,
title: T,
address: U,
) -> SendVenue
where
T: Into<String>,
U: Into<String>,
{
self.bot.send_venue(
self.update.chat.id,
latitude,
longitude,
title,
address,
)
}
pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote {
self.bot.send_video_note(self.update.chat.id, video_note)
}
pub fn answer_contact<T, U>(
&self,
phone_number: T,
first_name: U,
) -> SendContact
where
T: Into<String>,
U: Into<String>,
{
self.bot
.send_contact(self.chat_id(), phone_number, first_name)
}
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
self.bot.send_sticker(self.update.chat.id, sticker)
}
pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage
where
T: Into<ChatId>,
{
self.bot
.forward_message(chat_id, self.update.chat.id, self.update.id)
}
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
where
T: Into<String>,
{
self.bot.edit_message_text(
ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
},
text,
)
}
pub fn edit_message_caption(&self) -> EditMessageCaption {
self.bot.edit_message_caption(ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
})
}
pub fn delete_message(&self) -> DeleteMessage {
self.bot.delete_message(self.update.chat.id, self.update.id)
}
pub fn pin_message(&self) -> PinChatMessage {
self.bot
.pin_chat_message(self.update.chat.id, self.update.id)
}
}

View file

@ -0,0 +1,16 @@
/// Continue or terminate a dialogue.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum DialogueStage<D> {
Next(D),
Exit,
}
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
Ok(DialogueStage::Next(dialogue))
}
/// A shortcut for `Ok(DialogueStage::Exit)`.
pub fn exit<E, D>() -> Result<DialogueStage<D>, E> {
Ok(DialogueStage::Exit)
}

View file

@ -0,0 +1,13 @@
use crate::types::Message;
/// Something that has a chat ID.
pub trait GetChatId {
#[must_use]
fn chat_id(&self) -> i64;
}
impl GetChatId for Message {
fn chat_id(&self) -> i64 {
self.chat.id
}
}

View file

@ -0,0 +1,48 @@
//! Dealing with dialogues.
//!
//! There are four main components:
//!
//! 1. Your type `D`, which designates a dialogue state at the current
//! moment.
//! 2. [`Storage`], which encapsulates all the dialogues.
//! 3. Your handler, which receives an update and turns your dialogue into the
//! next state.
//! 4. [`DialogueDispatcher`], which encapsulates your handler, [`Storage`],
//! and implements [`CtxHandler`].
//!
//! You supply [`DialogueDispatcher`] into [`Dispatcher`]. Every time
//! [`Dispatcher`] calls `DialogueDispatcher::handle_ctx(...)`, the following
//! steps are executed:
//!
//! 1. If a storage doesn't contain a dialogue from this chat, supply
//! `D::default()` into you handler, otherwise, supply the saved session
//! from this chat.
//! 2. If a handler has returned [`DialogueStage::Exit`], remove the session
//! from the storage, otherwise ([`DialogueStage::Next`]) force the storage to
//! update the session.
//!
//! Please, see [examples/dialogue_bot] as an example.
//!
//! [`Storage`]: crate::dispatching::dialogue::Storage
//! [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
//! [`DialogueStage::Exit`]:
//! crate::dispatching::dialogue::DialogueStage::Exit
//! [`DialogueStage::Next`]: crate::dispatching::dialogue::DialogueStage::Next
//! [`CtxHandler`]: crate::dispatching::CtxHandler
//! [`Dispatcher`]: crate::dispatching::Dispatcher
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/dev/examples/dialogue_bot
#![allow(clippy::module_inception)]
#![allow(clippy::type_complexity)]
mod dialogue_dispatcher;
mod dialogue_handler_ctx;
mod dialogue_stage;
mod get_chat_id;
mod storage;
pub use dialogue_dispatcher::DialogueDispatcher;
pub use dialogue_handler_ctx::DialogueHandlerCtx;
pub use dialogue_stage::{exit, next, DialogueStage};
pub use get_chat_id::GetChatId;
pub use storage::{InMemStorage, Storage};

View file

@ -0,0 +1,29 @@
use async_trait::async_trait;
use super::Storage;
use std::collections::HashMap;
use tokio::sync::Mutex;
/// A memory storage based on a hash map. Stores all the dialogues directly in
/// RAM.
///
/// ## Note
/// All the dialogues will be lost after you restart your bot. If you need to
/// store them somewhere on a drive, you need to implement a storage
/// communicating with a DB.
#[derive(Debug, Default)]
pub struct InMemStorage<D> {
map: Mutex<HashMap<i64, D>>,
}
#[async_trait(?Send)]
#[async_trait]
impl<D> Storage<D> for InMemStorage<D> {
async fn remove_dialogue(&self, chat_id: i64) -> Option<D> {
self.map.lock().await.remove(&chat_id)
}
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D> {
self.map.lock().await.insert(chat_id, dialogue)
}
}

View file

@ -0,0 +1,28 @@
mod in_mem_storage;
use async_trait::async_trait;
pub use in_mem_storage::InMemStorage;
/// A storage of dialogues.
///
/// You can implement this trait for a structure that communicates with a DB and
/// be sure that after you restart your bot, all the dialogues won't be lost.
///
/// For a storage based on a simple hash map, see [`InMemStorage`].
///
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[async_trait(?Send)]
#[async_trait]
pub trait Storage<D> {
/// Removes a dialogue with the specified `chat_id`.
///
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
/// `dialogue` was deleted.
async fn remove_dialogue(&self, chat_id: i64) -> Option<D>;
/// Updates a dialogue with the specified `chat_id`.
///
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
/// `dialogue` was updated.
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D>;
}

View file

@ -0,0 +1,381 @@
use crate::{
dispatching::{
error_handlers::ErrorHandler, update_listeners,
update_listeners::UpdateListener, CtxHandler, DispatcherHandlerCtx,
DispatcherHandlerResult, LoggingErrorHandler,
},
types::{
CallbackQuery, ChosenInlineResult, InlineQuery, Message, Poll,
PollAnswer, PreCheckoutQuery, ShippingQuery, Update, UpdateKind,
},
Bot, RequestError,
};
use futures::{stream, StreamExt};
use std::{fmt::Debug, future::Future, sync::Arc};
type Handlers<'a, Upd, HandlerE> = Vec<
Box<
dyn CtxHandler<
DispatcherHandlerCtx<Upd>,
DispatcherHandlerResult<Upd, HandlerE>,
> + 'a,
>,
>;
/// One dispatcher to rule them all.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching).
// HandlerE=RequestError doesn't work now, because of very poor type inference.
// See https://github.com/rust-lang/rust/issues/27336 for more details.
pub struct Dispatcher<'a, HandlerE = RequestError> {
bot: Arc<Bot>,
handlers_error_handler: Box<dyn ErrorHandler<HandlerE> + 'a>,
update_handlers: Handlers<'a, Update, HandlerE>,
message_handlers: Handlers<'a, Message, HandlerE>,
edited_message_handlers: Handlers<'a, Message, HandlerE>,
channel_post_handlers: Handlers<'a, Message, HandlerE>,
edited_channel_post_handlers: Handlers<'a, Message, HandlerE>,
inline_query_handlers: Handlers<'a, InlineQuery, HandlerE>,
chosen_inline_result_handlers: Handlers<'a, ChosenInlineResult, HandlerE>,
callback_query_handlers: Handlers<'a, CallbackQuery, HandlerE>,
shipping_query_handlers: Handlers<'a, ShippingQuery, HandlerE>,
pre_checkout_query_handlers: Handlers<'a, PreCheckoutQuery, HandlerE>,
poll_handlers: Handlers<'a, Poll, HandlerE>,
poll_answer_handlers: Handlers<'a, PollAnswer, HandlerE>,
}
impl<'a, HandlerE> Dispatcher<'a, HandlerE>
where
HandlerE: Debug + 'a,
{
/// Constructs a new dispatcher with this `bot`.
#[must_use]
pub fn new(bot: Arc<Bot>) -> Self {
Self {
bot,
handlers_error_handler: Box::new(LoggingErrorHandler::new(
"An error from a Dispatcher's handler",
)),
update_handlers: Vec::new(),
message_handlers: Vec::new(),
edited_message_handlers: Vec::new(),
channel_post_handlers: Vec::new(),
edited_channel_post_handlers: Vec::new(),
inline_query_handlers: Vec::new(),
chosen_inline_result_handlers: Vec::new(),
callback_query_handlers: Vec::new(),
shipping_query_handlers: Vec::new(),
pre_checkout_query_handlers: Vec::new(),
poll_handlers: Vec::new(),
poll_answer_handlers: Vec::new(),
}
}
/// Registers a handler of errors, produced by other handlers.
#[must_use]
pub fn handlers_error_handler<T>(mut self, val: T) -> Self
where
T: ErrorHandler<HandlerE> + 'a,
{
self.handlers_error_handler = Box::new(val);
self
}
#[must_use]
pub fn update_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Update>, I> + 'a,
I: Into<DispatcherHandlerResult<Update, HandlerE>> + 'a,
{
self.update_handlers = register_handler(self.update_handlers, h);
self
}
#[must_use]
pub fn message_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Message>, I> + 'a,
I: Into<DispatcherHandlerResult<Message, HandlerE>> + 'a,
{
self.message_handlers = register_handler(self.message_handlers, h);
self
}
#[must_use]
pub fn edited_message_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Message>, I> + 'a,
I: Into<DispatcherHandlerResult<Message, HandlerE>> + 'a,
{
self.edited_message_handlers =
register_handler(self.edited_message_handlers, h);
self
}
#[must_use]
pub fn channel_post_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Message>, I> + 'a,
I: Into<DispatcherHandlerResult<Message, HandlerE>> + 'a,
{
self.channel_post_handlers =
register_handler(self.channel_post_handlers, h);
self
}
#[must_use]
pub fn edited_channel_post_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Message>, I> + 'a,
I: Into<DispatcherHandlerResult<Message, HandlerE>> + 'a,
{
self.edited_channel_post_handlers =
register_handler(self.edited_channel_post_handlers, h);
self
}
#[must_use]
pub fn inline_query_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<InlineQuery>, I> + 'a,
I: Into<DispatcherHandlerResult<InlineQuery, HandlerE>> + 'a,
{
self.inline_query_handlers =
register_handler(self.inline_query_handlers, h);
self
}
#[must_use]
pub fn chosen_inline_result_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<ChosenInlineResult>, I> + 'a,
I: Into<DispatcherHandlerResult<ChosenInlineResult, HandlerE>> + 'a,
{
self.chosen_inline_result_handlers =
register_handler(self.chosen_inline_result_handlers, h);
self
}
#[must_use]
pub fn callback_query_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<CallbackQuery>, I> + 'a,
I: Into<DispatcherHandlerResult<CallbackQuery, HandlerE>> + 'a,
{
self.callback_query_handlers =
register_handler(self.callback_query_handlers, h);
self
}
#[must_use]
pub fn shipping_query_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<ShippingQuery>, I> + 'a,
I: Into<DispatcherHandlerResult<ShippingQuery, HandlerE>> + 'a,
{
self.shipping_query_handlers =
register_handler(self.shipping_query_handlers, h);
self
}
#[must_use]
pub fn pre_checkout_query_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<PreCheckoutQuery>, I> + 'a,
I: Into<DispatcherHandlerResult<PreCheckoutQuery, HandlerE>> + 'a,
{
self.pre_checkout_query_handlers =
register_handler(self.pre_checkout_query_handlers, h);
self
}
#[must_use]
pub fn poll_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<Poll>, I> + 'a,
I: Into<DispatcherHandlerResult<Poll, HandlerE>> + 'a,
{
self.poll_handlers = register_handler(self.poll_handlers, h);
self
}
#[must_use]
pub fn poll_answer_handler<H, I>(mut self, h: &'a H) -> Self
where
H: CtxHandler<DispatcherHandlerCtx<PollAnswer>, I> + 'a,
I: Into<DispatcherHandlerResult<PollAnswer, HandlerE>> + 'a,
{
self.poll_answer_handlers =
register_handler(self.poll_answer_handlers, h);
self
}
/// Starts your bot with the default parameters.
///
/// The default parameters are a long polling update listener and log all
/// errors produced by this listener).
pub async fn dispatch(&'a self) {
self.dispatch_with_listener(
update_listeners::polling_default(Arc::clone(&self.bot)),
&LoggingErrorHandler::new("An error from the update listener"),
)
.await;
}
/// Starts your bot with custom `update_listener` and
/// `update_listener_error_handler`.
pub async fn dispatch_with_listener<UListener, ListenerE, Eh>(
&'a self,
update_listener: UListener,
update_listener_error_handler: &'a Eh,
) where
UListener: UpdateListener<ListenerE> + 'a,
Eh: ErrorHandler<ListenerE> + 'a,
ListenerE: Debug,
{
let update_listener = Box::pin(update_listener);
update_listener
.for_each_concurrent(None, move |update| async move {
log::trace!("Dispatcher received an update: {:?}", update);
let update = match update {
Ok(update) => update,
Err(error) => {
update_listener_error_handler.handle_error(error).await;
return;
}
};
let update =
match self.handle(&self.update_handlers, update).await {
Some(update) => update,
None => return,
};
match update.kind {
UpdateKind::Message(message) => {
self.handle(&self.message_handlers, message).await;
}
UpdateKind::EditedMessage(message) => {
self.handle(&self.edited_message_handlers, message)
.await;
}
UpdateKind::ChannelPost(post) => {
self.handle(&self.channel_post_handlers, post).await;
}
UpdateKind::EditedChannelPost(post) => {
self.handle(&self.edited_channel_post_handlers, post)
.await;
}
UpdateKind::InlineQuery(query) => {
self.handle(&self.inline_query_handlers, query).await;
}
UpdateKind::ChosenInlineResult(result) => {
self.handle(
&self.chosen_inline_result_handlers,
result,
)
.await;
}
UpdateKind::CallbackQuery(query) => {
self.handle(&self.callback_query_handlers, query).await;
}
UpdateKind::ShippingQuery(query) => {
self.handle(&self.shipping_query_handlers, query).await;
}
UpdateKind::PreCheckoutQuery(query) => {
self.handle(&self.pre_checkout_query_handlers, query)
.await;
}
UpdateKind::Poll(poll) => {
self.handle(&self.poll_handlers, poll).await;
}
UpdateKind::PollAnswer(answer) => {
self.handle(&self.poll_answer_handlers, answer).await;
}
}
})
.await
}
// Handles a single update.
#[allow(clippy::ptr_arg)]
async fn handle<Upd>(
&self,
handlers: &Handlers<'a, Upd, HandlerE>,
update: Upd,
) -> Option<Upd> {
stream::iter(handlers)
.fold(Some(update), |acc, handler| {
async move {
// Option::and_then is not working here, because
// Middleware::handle is asynchronous.
match acc {
Some(update) => {
let DispatcherHandlerResult { next, result } =
handler
.handle_ctx(DispatcherHandlerCtx {
bot: Arc::clone(&self.bot),
update,
})
.await;
if let Err(error) = result {
self.handlers_error_handler
.handle_error(error)
.await
}
next
}
None => None,
}
}
})
.await
}
}
/// Transforms Future<Output = T> into Future<Output = U> by applying an Into
/// conversion.
async fn intermediate_fut0<T, U>(fut: impl Future<Output = T>) -> U
where
T: Into<U>,
{
fut.await.into()
}
/// Transforms CtxHandler with Into<DispatcherHandlerResult<...>> as a return
/// value into CtxHandler with DispatcherHandlerResult return value.
fn intermediate_fut1<'a, Upd, HandlerE, H, I>(
h: &'a H,
) -> impl CtxHandler<
DispatcherHandlerCtx<Upd>,
DispatcherHandlerResult<Upd, HandlerE>,
> + 'a
where
H: CtxHandler<DispatcherHandlerCtx<Upd>, I> + 'a,
I: Into<DispatcherHandlerResult<Upd, HandlerE>> + 'a,
Upd: 'a,
{
move |ctx| intermediate_fut0(h.handle_ctx(ctx))
}
/// Registers a single handler.
fn register_handler<'a, Upd, H, I, HandlerE>(
mut handlers: Handlers<'a, Upd, HandlerE>,
h: &'a H,
) -> Handlers<'a, Upd, HandlerE>
where
H: CtxHandler<DispatcherHandlerCtx<Upd>, I> + 'a,
I: Into<DispatcherHandlerResult<Upd, HandlerE>> + 'a,
HandlerE: 'a,
Upd: 'a,
{
handlers.push(Box::new(intermediate_fut1(h)));
handlers
}

View file

@ -0,0 +1,168 @@
use crate::{
dispatching::dialogue::GetChatId,
requests::{
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
SendVenue, SendVideo, SendVideoNote, SendVoice,
},
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
Bot,
};
use std::sync::Arc;
/// A [`Dispatcher`]'s handler's context of a bot and an update.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching).
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub struct DispatcherHandlerCtx<Upd> {
pub bot: Arc<Bot>,
pub update: Upd,
}
impl<Upd> GetChatId for DispatcherHandlerCtx<Upd>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.update.chat_id()
}
}
impl DispatcherHandlerCtx<Message> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot.send_message(self.chat_id(), text)
}
pub fn reply_to<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot
.send_message(self.chat_id(), text)
.reply_to_message_id(self.update.id)
}
pub fn answer_photo(&self, photo: InputFile) -> SendPhoto {
self.bot.send_photo(self.update.chat.id, photo)
}
pub fn answer_audio(&self, audio: InputFile) -> SendAudio {
self.bot.send_audio(self.update.chat.id, audio)
}
pub fn answer_animation(&self, animation: InputFile) -> SendAnimation {
self.bot.send_animation(self.update.chat.id, animation)
}
pub fn answer_document(&self, document: InputFile) -> SendDocument {
self.bot.send_document(self.update.chat.id, document)
}
pub fn answer_video(&self, video: InputFile) -> SendVideo {
self.bot.send_video(self.update.chat.id, video)
}
pub fn answer_voice(&self, voice: InputFile) -> SendVoice {
self.bot.send_voice(self.update.chat.id, voice)
}
pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup
where
T: Into<Vec<InputMedia>>,
{
self.bot.send_media_group(self.update.chat.id, media_group)
}
pub fn answer_location(
&self,
latitude: f32,
longitude: f32,
) -> SendLocation {
self.bot
.send_location(self.update.chat.id, latitude, longitude)
}
pub fn answer_venue<T, U>(
&self,
latitude: f32,
longitude: f32,
title: T,
address: U,
) -> SendVenue
where
T: Into<String>,
U: Into<String>,
{
self.bot.send_venue(
self.update.chat.id,
latitude,
longitude,
title,
address,
)
}
pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote {
self.bot.send_video_note(self.update.chat.id, video_note)
}
pub fn answer_contact<T, U>(
&self,
phone_number: T,
first_name: U,
) -> SendContact
where
T: Into<String>,
U: Into<String>,
{
self.bot
.send_contact(self.chat_id(), phone_number, first_name)
}
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
self.bot.send_sticker(self.update.chat.id, sticker)
}
pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage
where
T: Into<ChatId>,
{
self.bot
.forward_message(chat_id, self.update.chat.id, self.update.id)
}
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
where
T: Into<String>,
{
self.bot.edit_message_text(
ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
},
text,
)
}
pub fn edit_message_caption(&self) -> EditMessageCaption {
self.bot.edit_message_caption(ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
})
}
pub fn delete_message(&self) -> DeleteMessage {
self.bot.delete_message(self.update.chat.id, self.update.id)
}
pub fn pin_message(&self) -> PinChatMessage {
self.bot
.pin_chat_message(self.update.chat.id, self.update.id)
}
}

View file

@ -0,0 +1,31 @@
/// A result of a handler in [`Dispatcher`].
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching).
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub struct DispatcherHandlerResult<Upd, E> {
pub next: Option<Upd>,
pub result: Result<(), E>,
}
impl<Upd, E> DispatcherHandlerResult<Upd, E> {
/// Creates new `DispatcherHandlerResult` that continues the pipeline.
pub fn next(update: Upd, result: Result<(), E>) -> Self {
Self {
next: Some(update),
result,
}
}
/// Creates new `DispatcherHandlerResult` that terminates the pipeline.
pub fn exit(result: Result<(), E>) -> Self {
Self { next: None, result }
}
}
impl<Upd, E> From<Result<(), E>> for DispatcherHandlerResult<Upd, E> {
fn from(result: Result<(), E>) -> Self {
Self::exit(result)
}
}

View file

@ -0,0 +1,151 @@
use std::{convert::Infallible, fmt::Debug, future::Future, pin::Pin};
/// An asynchronous handler of an error.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching).
pub trait ErrorHandler<E> {
#[must_use]
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a;
}
impl<E, F, Fut> ErrorHandler<E> for F
where
F: Fn(E) -> Fut,
Fut: Future<Output = ()>,
{
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
Box::pin(async move { self(error).await })
}
}
/// A handler that silently ignores all errors.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::{ErrorHandler, IgnoringErrorHandler};
///
/// IgnoringErrorHandler.handle_error(()).await;
/// IgnoringErrorHandler.handle_error(404).await;
/// IgnoringErrorHandler.handle_error("error").await;
/// # }
/// ```
pub struct IgnoringErrorHandler;
impl<E> ErrorHandler<E> for IgnoringErrorHandler {
fn handle_error<'a>(
&'a self,
_: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
Box::pin(async {})
}
}
/// A handler that silently ignores all errors that can never happen (e.g.:
/// [`!`] or [`Infallible`]).
///
/// ## Examples
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use std::convert::{Infallible, TryInto};
///
/// use teloxide::dispatching::{ErrorHandler, IgnoringErrorHandlerSafe};
///
/// let result: Result<String, Infallible> = "str".try_into();
/// match result {
/// Ok(string) => println!("{}", string),
/// Err(inf) => IgnoringErrorHandlerSafe.handle_error(inf).await,
/// }
///
/// IgnoringErrorHandlerSafe.handle_error(return).await; // return type of `return` is `!` (aka never)
/// # }
/// ```
///
/// ```compile_fail
/// use teloxide::dispatching::{ErrorHandler, IgnoringErrorHandlerSafe};
///
/// IgnoringErrorHandlerSafe.handle_error(0);
/// ```
///
/// [`!`]: https://doc.rust-lang.org/std/primitive.never.html
/// [`Infallible`]: std::convert::Infallible
pub struct IgnoringErrorHandlerSafe;
#[allow(unreachable_code)]
impl ErrorHandler<Infallible> for IgnoringErrorHandlerSafe {
fn handle_error<'a>(
&'a self,
_: Infallible,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
Infallible: 'a,
{
Box::pin(async {})
}
}
/// A handler that log all errors passed into it.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::{ErrorHandler, LoggingErrorHandler};
///
/// LoggingErrorHandler::default().handle_error(()).await;
/// LoggingErrorHandler::new("error").handle_error(404).await;
/// LoggingErrorHandler::new("error")
/// .handle_error("Invalid data type!")
/// .await;
/// # }
/// ```
#[derive(Default)]
pub struct LoggingErrorHandler {
text: String,
}
impl LoggingErrorHandler {
/// Creates `LoggingErrorHandler` with a meta text before a log.
///
/// The logs will be printed in this format: `{text}: {:?}`.
#[must_use]
pub fn new<T>(text: T) -> Self
where
T: Into<String>,
{
Self { text: text.into() }
}
}
impl<E> ErrorHandler<E> for LoggingErrorHandler
where
E: Debug,
{
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
log::error!("{text}: {:?}", error, text = self.text);
Box::pin(async {})
}
}

120
src/dispatching/mod.rs Normal file
View file

@ -0,0 +1,120 @@
//! Updates dispatching.
//!
//! The key type here is [`Dispatcher`]. It encapsulates [`Bot`], handlers for
//! [11 update kinds] (+ for [`Update`]) and [`ErrorHandler`] for them. When
//! [`Update`] is received from Telegram, the following steps are executed:
//!
//! 1. It is supplied into an appropriate handler (the first ones is those who
//! accept [`Update`]).
//! 2. If a handler failed, invoke [`ErrorHandler`] with the corresponding
//! error.
//! 3. If a handler has returned [`DispatcherHandlerResult`] with `None`,
//! terminate the pipeline, otherwise supply an update into the next handler
//! (back to step 1).
//!
//! The pipeline is executed until either all the registered handlers were
//! executed, or one of handlers has terminated the pipeline. That's simple!
//!
//! 1. Note that handlers implement [`CtxHandler`], which means that you are
//! able to supply [`DialogueDispatcher`] as a handler, since it implements
//! [`CtxHandler`] too!
//! 2. Note that you don't always need to return [`DispatcherHandlerResult`]
//! explicitly, because of automatic conversions. Just return `Result<(), E>` if
//! you want to terminate the pipeline (see the example below).
//!
//! # Examples
//! ### The ping-pong bot
//!
//! ```no_run
//! # #[tokio::main]
//! # async fn main_() {
//! use teloxide::prelude::*;
//!
//! // Setup logging here...
//!
//! // Create a dispatcher with a single message handler that answers "pong"
//! // to each incoming message.
//! Dispatcher::<RequestError>::new(Bot::from_env())
//! .message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
//! ctx.answer("pong").send().await?;
//! Ok(())
//! })
//! .dispatch()
//! .await;
//! # }
//! ```
//!
//! [Full](https://github.com/teloxide/teloxide/blob/dev/examples/ping_pong_bot/)
//!
//! ### Multiple handlers
//!
//! ```no_run
//! # #[tokio::main]
//! # async fn main_() {
//! use teloxide::prelude::*;
//!
//! // Create a dispatcher with multiple handlers of different types. This will
//! // print One! and Two! on every incoming UpdateKind::Message.
//! Dispatcher::<RequestError>::new(Bot::from_env())
//! // This is the first UpdateKind::Message handler, which will be called
//! // after the Update handler below.
//! .message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
//! log::info!("Two!");
//! DispatcherHandlerResult::next(ctx.update, Ok(()))
//! })
//! // Remember: handler of Update are called first.
//! .update_handler(&|ctx: DispatcherHandlerCtx<Update>| async move {
//! log::info!("One!");
//! DispatcherHandlerResult::next(ctx.update, Ok(()))
//! })
//! // This handler will be called right after the first UpdateKind::Message
//! // handler, because it is registered after.
//! .message_handler(&|_ctx: DispatcherHandlerCtx<Message>| async move {
//! // The same as DispatcherHandlerResult::exit(Ok(()))
//! Ok(())
//! })
//! // This handler will never be called, because the UpdateKind::Message
//! // handler above terminates the pipeline.
//! .message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
//! log::info!("This will never be printed!");
//! DispatcherHandlerResult::next(ctx.update, Ok(()))
//! })
//! .dispatch()
//! .await;
//!
//! // Note: if this bot receive, for example, UpdateKind::ChannelPost, it will
//! // only print "One!", because the UpdateKind::Message handlers will not be
//! // called.
//! # }
//! ```
//!
//! [Full](https://github.com/teloxide/teloxide/blob/dev/examples/miltiple_handlers_bot/)
//!
//! For a bit more complicated example, please see [examples/dialogue_bot].
//!
//! [`Dispatcher`]: crate::dispatching::Dispatcher
//! [11 update kinds]: crate::types::UpdateKind
//! [`Update`]: crate::types::Update
//! [`ErrorHandler`]: crate::dispatching::ErrorHandler
//! [`CtxHandler`]: crate::dispatching::CtxHandler
//! [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
//! [`DispatcherHandlerResult`]: crate::dispatching::DispatcherHandlerResult
//! [`Bot`]: crate::Bot
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/dev/examples/dialogue_bot
mod ctx_handlers;
pub mod dialogue;
mod dispatcher;
mod dispatcher_handler_ctx;
mod dispatcher_handler_result;
mod error_handlers;
pub mod update_listeners;
pub use ctx_handlers::CtxHandler;
pub use dispatcher::Dispatcher;
pub use dispatcher_handler_ctx::DispatcherHandlerCtx;
pub use dispatcher_handler_result::DispatcherHandlerResult;
pub use error_handlers::{
ErrorHandler, IgnoringErrorHandler, IgnoringErrorHandlerSafe,
LoggingErrorHandler,
};

View file

@ -0,0 +1,204 @@
//! Receiving updates from Telegram.
//!
//! The key trait here is [`UpdateListener`]. You can get it by these functions:
//!
//! - [`polling_default`], which returns a default long polling listener.
//! - [`polling`], which returns a long/short polling listener with your
//! configuration.
//!
//! And then you can extract updates from it and pass them directly to a
//! dispatcher.
//!
//! Telegram supports two ways of [getting updates]: [long]/[short] polling and
//! [webhook].
//!
//! # Long Polling
//!
//! In long polling, you just call [`Box::get_updates`] every N seconds.
//!
//! ## Example
//!
//! <pre>
//! tg bot
//! | |
//! |<---------------------------| Updates? (Bot::get_updates call)
//! ↑ ↑
//! | timeout<a id="1b" href="#1">^1</a> |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay between Bot::get_updates<a id="2b" href="#2">^2</a> |
//! ↓ ↓
//! |<---------------------------| Updates?
//! ↑ ↑
//! | timeout<a id="3b" href="#3">^3</a> |
//! ↓ ↓
//! Yes |-------[updates 0, 1]------>|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 1]--------| Updates?<a id="4b" href="#4">^4</a>
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Yes |---------[update 2]-------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Yes |-------[updates 2..5]------>|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 5]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! | |
//! ~ and so on, and so on ~
//! </pre>
//!
//! <a id="1" href="#1b">^1</a> A timeout can be even 0
//! (this is also called short polling),
//! but you should use it **only** for testing purposes.
//!
//! <a id="2" href="#2b">^2</a> Large delays will cause in bot lags,
//! so delay shouldn't exceed second.
//!
//! <a id="3" href="#3b">^3</a> Note that if Telegram already have updates for
//! you it will answer you **without** waiting for a timeout.
//!
//! <a id="4" href="#4b">^4</a> `offset = N` means that we've already received
//! updates `0..=N`.
//!
//! [`UpdateListener`]: UpdateListener
//! [`polling_default`]: polling_default
//! [`polling`]: polling
//! [`Box::get_updates`]: crate::Bot::get_updates
//! [getting updates]: https://core.telegram.org/bots/api#getting-updates
//! [long]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
//! [short]: https://en.wikipedia.org/wiki/Polling_(computer_science)
//! [webhook]: https://en.wikipedia.org/wiki/Webhook
use futures::{stream, Stream, StreamExt};
use crate::{
bot::Bot,
requests::Request,
types::{AllowedUpdate, Update},
RequestError,
};
use std::{convert::TryInto, sync::Arc, time::Duration};
/// A generic update listener.
pub trait UpdateListener<E>: Stream<Item = Result<Update, E>> {
// TODO: add some methods here (.shutdown(), etc).
}
impl<S, E> UpdateListener<E> for S where S: Stream<Item = Result<Update, E>> {}
/// Returns a long polling update listener with the default configuration.
///
/// See also: [`polling`](polling).
pub fn polling_default(bot: Arc<Bot>) -> impl UpdateListener<RequestError> {
polling(bot, None, None, None)
}
/// Returns a long/short polling update listener with some additional options.
///
/// - `bot`: Using this bot, the returned update listener will receive updates.
/// - `timeout`: A timeout for polling.
/// - `limit`: Limits the number of updates to be retrieved at once. Values
/// between 1—100 are accepted.
/// - `allowed_updates`: A list the types of updates you want to receive.
/// See [`GetUpdates`] for defaults.
///
/// See also: [`polling_default`](polling_default).
///
/// [`GetUpdates`]: crate::requests::GetUpdates
pub fn polling(
bot: Arc<Bot>,
timeout: Option<Duration>,
limit: Option<u8>,
allowed_updates: Option<Vec<AllowedUpdate>>,
) -> impl UpdateListener<RequestError> {
let timeout =
timeout.map(|t| t.as_secs().try_into().expect("timeout is too big"));
stream::unfold(
(allowed_updates, bot, 0),
move |(mut allowed_updates, bot, mut offset)| async move {
let mut req = bot.get_updates().offset(offset);
req.timeout = timeout;
req.limit = limit;
req.allowed_updates = allowed_updates.take();
let updates = match req.send().await {
Err(err) => vec![Err(err)],
Ok(updates) => {
// Set offset to the last update's id + 1
if let Some(upd) = updates.last() {
let id: i32 = match upd {
Ok(ok) => ok.id,
Err((value, _)) => value["update_id"]
.as_i64()
.expect(
"The 'update_id' field must always exist in \
Update",
)
.try_into()
.expect("update_id must be i32"),
};
offset = id + 1;
}
let updates = updates
.into_iter()
.filter(|update| match update {
Err((value, error)) => {
log::error!("Cannot parse an update.\nError: {:?}\nValue: {}\n\
This is a bug in teloxide, please open an issue here: \
https://github.com/teloxide/teloxide/issues.", error, value);
false
}
Ok(_) => true,
})
.map(|update| {
update.expect("See the previous .filter() call")
})
.collect::<Vec<Update>>();
updates.into_iter().map(Ok).collect::<Vec<_>>()
}
};
Some((stream::iter(updates), (allowed_updates, bot, offset)))
},
)
.flatten()
}
// TODO implement webhook (this actually require webserver and probably we
// should add cargo feature that adds webhook)
//pub fn webhook<'a>(bot: &'a cfg: WebhookConfig) -> Updater<impl
// Stream<Item=Result<Update, ???>> + 'a> {}

519
src/errors.rs Normal file
View file

@ -0,0 +1,519 @@
use derive_more::From;
use reqwest::StatusCode;
use serde::Deserialize;
use thiserror::Error;
//<editor-fold desc="download">
/// An error occurred after downloading a file.
#[derive(Debug, Error, From)]
pub enum DownloadError {
#[error("A network error: {0}")]
NetworkError(#[source] reqwest::Error),
#[error("An I/O error: {0}")]
Io(#[source] std::io::Error),
}
//</editor-fold>
//<editor-fold desc="request">
/// An error occurred after making a request to Telegram.
#[derive(Debug, Error)]
pub enum RequestError {
#[error("A Telegram's error #{status_code}: {kind:?}")]
ApiError {
status_code: StatusCode,
kind: ApiErrorKind,
},
/// The group has been migrated to a supergroup with the specified
/// identifier.
#[error("The group has been migrated to a supergroup with ID #{0}")]
MigrateToChatId(i64),
/// In case of exceeding flood control, the number of seconds left to wait
/// before the request can be repeated.
#[error("Retry after {0} seconds")]
RetryAfter(i32),
#[error("A network error: {0}")]
NetworkError(#[source] reqwest::Error),
#[error("An error while parsing JSON: {0}")]
InvalidJson(#[source] serde_json::Error),
}
//</editor-fold>
/// A kind of an API error returned from Telegram.
#[derive(Debug, Deserialize, PartialEq, Copy, Hash, Eq, Clone)]
pub enum ApiErrorKind {
/// Occurs when the bot tries to send message to user who blocked the bot.
#[serde(rename = "Forbidden: bot was blocked by the user")]
BotBlocked,
/// Occurs when bot tries to modify a message without modification content.
///
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message is not modified: specified new \
message content and reply markup are exactly the same \
as a current content and reply markup of the message")]
MessageNotModified,
/// Occurs when bot tries to forward or delete a message which was deleted.
///
/// May happen in methods:
/// 1. [`ForwardMessage`]
/// 2. [`DeleteMessage`]
///
/// [`ForwardMessage`]: crate::requests::ForwardMessage
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: MESSAGE_ID_INVALID")]
MessageIdInvalid,
/// Occurs when bot tries to forward a message which does not exists.
///
/// May happen in methods:
/// 1. [`ForwardMessage`]
///
/// [`ForwardMessage`]: crate::requests::ForwardMessage
#[serde(rename = "Bad Request: message to forward not found")]
MessageToForwardNotFound,
/// Occurs when bot tries to delete a message which does not exists.
///
/// May happen in methods:
/// 1. [`DeleteMessage`]
///
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: message to delete not found")]
MessageToDeleteNotFound,
/// Occurs when bot tries to send a text message without text.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: message text is empty")]
MessageTextIsEmpty,
/// Occurs when bot tries to edit a message after long time.
///
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message can't be edited")]
MessageCantBeEdited,
/// Occurs when bot tries to delete a someone else's message in group where
/// it does not have enough rights.
///
/// May happen in methods:
/// 1. [`DeleteMessage`]
///
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: message can't be deleted")]
MessageCantBeDeleted,
/// Occurs when bot tries to edit a message which does not exists.
///
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message to edit not found")]
MessageToEditNotFound,
/// Occurs when bot tries to reply to a message which does not exists.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: reply message not found")]
MessageToReplyNotFound,
/// Occurs when bot tries to
#[serde(rename = "Bad Request: message identifier is not specified")]
MessageIdentifierNotSpecified,
/// Occurs when bot tries to send a message with text size greater then
/// 4096 symbols.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: message is too long")]
MessageIsTooLong,
/// Occurs when bot tries to send media group with more than 10 items.
///
/// May happen in methods:
/// 1. [`SendMediaGroup`]
///
/// [`SendMediaGroup`]: crate::requests::SendMediaGroup
#[serde(rename = "Bad Request: Too much messages to send as an album")]
ToMuchMessages,
/// Occurs when bot tries to stop poll that has already been stopped.
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll has already been closed")]
PollHasAlreadyClosed,
/// Occurs when bot tries to send poll with less than 2 options.
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll must have at least 2 option")]
PollMustHaveMoreOptions,
/// Occurs when bot tries to send poll with more than 10 options.
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll can't have more than 10 options")]
PollCantHaveMoreOptions,
/// Occurs when bot tries to send poll with empty option (without text).
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll options must be non-empty")]
PollOptionsMustBeNonEmpty,
/// Occurs when bot tries to send poll with empty question (without text).
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll question must be non-empty")]
PollQuestionMustBeNonEmpty,
/// Occurs when bot tries to send poll with total size of options more than
/// 100 symbols.
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll options length must not exceed 100")]
PollOptionsLengthTooLong,
/// Occurs when bot tries to send poll with question size more than 255
/// symbols.
///
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll question length must not exceed 255")]
PollQuestionLengthTooLong,
/// Occurs when bot tries to stop poll with message without poll.
///
/// May happen in methods:
/// 1. [`StopPoll`]
///
/// [`StopPoll`]: crate::requests::StopPoll
#[serde(rename = "Bad Request: message with poll to stop not found")]
MessageWithPollNotFound,
/// Occurs when bot tries to stop poll with message without poll.
///
/// May happen in methods:
/// 1. [`StopPoll`]
///
/// [`StopPoll`]: crate::requests::StopPoll
#[serde(rename = "Bad Request: message is not a poll")]
MessageIsNotAPoll,
/// Occurs when bot tries to send a message to chat in which it is not a
/// member.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: chat not found")]
ChatNotFound,
/// Occurs when bot tries to send method with unknown user_id.
///
/// May happen in methods:
/// 1. [`getUserProfilePhotos`]
///
/// [`getUserProfilePhotos`]:
/// crate::requests::GetUserProfilePhotos
#[serde(rename = "Bad Request: user not found")]
UserNotFound,
/// Occurs when bot tries to send [`SetChatDescription`] with same text as
/// in the current description.
///
/// May happen in methods:
/// 1. [`SetChatDescription`]
///
/// [`SetChatDescription`]: crate::requests::SetChatDescription
#[serde(rename = "Bad Request: chat description is not modified")]
ChatDescriptionIsNotModified,
/// Occurs when bot tries to answer to query after timeout expire.
///
/// May happen in methods:
/// 1. [`AnswerCallbackQuery`]
///
/// [`AnswerCallbackQuery`]: crate::requests::AnswerCallbackQuery
#[serde(rename = "Bad Request: query is too old and response timeout \
expired or query id is invalid")]
InvalidQueryID,
/// Occurs when bot tries to send InlineKeyboardMarkup with invalid button
/// url.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: BUTTON_URL_INVALID")]
ButtonURLInvalid,
/// Occurs when bot tries to send button with data size more than 64 bytes.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: BUTTON_DATA_INVALID")]
ButtonDataInvalid,
/// Occurs when bot tries to send button with data size == 0.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: can't parse inline keyboard button: Text \
buttons are unallowed in the inline keyboard")]
TextButtonsAreUnallowed,
/// Occurs when bot tries to get file by wrong file id.
///
/// May happen in methods:
/// 1. [`GetFile`]
///
/// [`GetFile`]: crate::requests::GetFile
#[serde(rename = "Bad Request: wrong file id")]
WrongFileID,
/// Occurs when bot tries to do some with group which was deactivated.
#[serde(rename = "Bad Request: group is deactivated")]
GroupDeactivated,
/// Occurs when bot tries to set chat photo from file ID
///
/// May happen in methods:
/// 1. [`SetChatPhoto`]
///
/// [`SetChatPhoto`]: crate::requests::SetChatPhoto
#[serde(rename = "Bad Request: Photo should be uploaded as an InputFile")]
PhotoAsInputFileRequired,
/// Occurs when bot tries to add sticker to stickerset by invalid name.
///
/// May happen in methods:
/// 1. [`AddStickerToSet`]
///
/// [`AddStickerToSet`]: crate::requests::AddStickerToSet
#[serde(rename = "Bad Request: STICKERSET_INVALID")]
InvalidStickersSet,
/// Occurs when bot tries to pin a message without rights to pin in this
/// chat.
///
/// May happen in methods:
/// 1. [`PinChatMessage`]
///
/// [`PinChatMessage`]: crate::requests::PinChatMessage
#[serde(rename = "Bad Request: not enough rights to pin a message")]
NotEnoughRightsToPinMessage,
/// Occurs when bot tries to use method in group which is allowed only in a
/// supergroup or channel.
#[serde(rename = "Bad Request: method is available only for supergroups \
and channel")]
MethodNotAvailableInPrivateChats,
/// Occurs when bot tries to demote chat creator.
///
/// May happen in methods:
/// 1. [`PromoteChatMember`]
///
/// [`PromoteChatMember`]: crate::requests::PromoteChatMember
#[serde(rename = "Bad Request: can't demote chat creator")]
CantDemoteChatCreator,
/// Occurs when bot tries to restrict self in group chats.
///
/// May happen in methods:
/// 1. [`RestrictChatMember`]
///
/// [`RestrictChatMember`]: crate::requests::RestrictChatMember
#[serde(rename = "Bad Request: can't restrict self")]
CantRestrictSelf,
/// Occurs when bot tries to restrict chat member without rights to
/// restrict in this chat.
///
/// May happen in methods:
/// 1. [`RestrictChatMember`]
///
/// [`RestrictChatMember`]: crate::requests::RestrictChatMember
#[serde(rename = "Bad Request: not enough rights to restrict/unrestrict \
chat member")]
NotEnoughRightsToRestrict,
/// Occurs when bot tries set webhook to protocol other than HTTPS.
///
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: HTTPS url must be provided \
for webhook")]
WebhookRequireHTTPS,
/// Occurs when bot tries to set webhook to port other than 80, 88, 443 or
/// 8443.
///
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: Webhook can be set up only \
on ports 80, 88, 443 or 8443")]
BadWebhookPort,
/// Occurs when bot tries to set webhook to unknown host.
///
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: Failed to resolve host: \
Name or service not known")]
UnknownHost,
/// Occurs when bot tries to set webhook to invalid URL.
///
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: can't parse URL")]
CantParseUrl,
/// Occurs when bot tries to send message with unfinished entities.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: can't parse entities")]
CantParseEntities,
/// Occurs when bot tries to use getUpdates while webhook is active.
///
/// May happen in methods:
/// 1. [`GetUpdates`]
///
/// [`GetUpdates`]: crate::requests::GetUpdates
#[serde(rename = "can't use getUpdates method while webhook is active")]
CantGetUpdates,
/// Occurs when bot tries to do some in group where bot was kicked.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: bot was kicked from a chat")]
BotKicked,
/// Occurs when bot tries to send message to deactivated user.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: user is deactivated")]
UserDeactivated,
/// Occurs when you tries to initiate conversation with a user.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(
rename = "Unauthorized: bot can't initiate conversation with a user"
)]
CantInitiateConversation,
/// Occurs when you tries to send message to bot.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: bot can't send messages to bots")]
CantTalkWithBots,
/// Occurs when bot tries to send button with invalid http url.
///
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: wrong HTTP URL")]
WrongHTTPurl,
/// Occurs when bot tries GetUpdate before the timeout. Make sure that only
/// one Updater is running.
///
/// May happen in methods:
/// 1. [`GetUpdates`]
///
/// [`GetUpdates`]: crate::requests::GetUpdates
#[serde(rename = "Conflict: terminated by other getUpdates request; \
make sure that only one bot instance is running")]
TerminatedByOtherGetUpdates,
/// Occurs when bot tries to get file by invalid file id.
///
/// May happen in methods:
/// 1. [`GetFile`]
///
/// [`GetFile`]: crate::requests::GetFile
#[serde(rename = "Bad Request: invalid file id")]
FileIdInvalid,
#[serde(other)]
Other,
}

View file

@ -1,6 +1,299 @@
#![feature(async_await)]
//! A full-featured framework that empowers you to easily build [Telegram bots]
//! using the [`async`/`.await`] syntax in [Rust]. It handles all the difficult
//! stuff so you can focus only on your business logic.
//!
//! ## Features
//! - **Type-safe.** teloxide leverages the Rust's type system with two serious
//! implications: resistance to human mistakes and tight integration with
//! IDEs. Write fast, avoid debugging as possible.
//!
//! - **Persistency.** By default, teloxide stores all user dialogues in RAM,
//! but you can store them somewhere else (for example, in DB) just by
//! implementing 2 functions.
//!
//! - **Convenient dialogues system.** Define a type-safe [finite automaton]
//! and transition functions to drive a user dialogue with ease (see the
//! examples below).
//!
//! - **Convenient API.** Automatic conversions are used to avoid boilerplate.
//! For example, functions accept `Into<String>`, rather than `&str` or
//! `String`, so you can call them without `.to_string()`/`.as_str()`/etc.
//!
//! ## Getting started
//! 1. Create a new bot using [@Botfather] to get a token in the format
//! `123456789:blablabla`. 2. Initialise the `TELOXIDE_TOKEN` environmental
//! variable to your token:
//! ```bash
//! # Unix
//! $ export TELOXIDE_TOKEN=MyAwesomeToken
//!
//! # Windows
//! $ set TELOXITE_TOKEN=MyAwesomeToken
//! ```
//! 3. Be sure that you are up to date:
//! ```bash
//! $ rustup update stable
//! ```
//!
//! 4. Execute `cargo new my_bot`, enter the directory and put these lines into
//! your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! teloxide = "0.1.0"
//! log = "0.4.8"
//! tokio = "0.2.11"
//! pretty_env_logger = "0.4.0"
//! ```
//!
//! ## The ping-pong bot
//! This bot has a single message handler, which answers "pong" to each incoming
//! message:
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/dev/examples/ping_pong_bot/src/main.rs))
//! ```rust,no_run
//! use teloxide::prelude::*;
//!
//! # #[tokio::main]
//! # async fn main() {
//! teloxide::enable_logging!();
//! log::info!("Starting the ping-pong bot!");
//!
//! let bot = Bot::from_env();
//!
//! Dispatcher::<RequestError>::new(bot)
//! .message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
//! ctx.answer("pong").send().await?;
//! Ok(())
//! })
//! .dispatch()
//! .await;
//! # }
//! ```
//!
//! ## Commands
//! Commands are defined similar to how we define CLI using [structopt]. This
//! bot says "I am a cat! Meow!" on `/meow`, generates a random number within
//! [0; 1) on `/generate`, and shows the usage guide on `/help`:
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/dev/examples/simple_commands_bot/src/main.rs))
//! ```rust,no_run
//! # use teloxide::{prelude::*, utils::command::BotCommand};
//! # use rand::{thread_rng, Rng};
//! // Imports are omitted...
//!
//! #[derive(BotCommand)]
//! #[command(
//! rename = "lowercase",
//! description = "These commands are supported:"
//! )]
//! enum Command {
//! #[command(description = "display this text.")]
//! Help,
//! #[command(description = "be a cat.")]
//! Meow,
//! #[command(description = "generate a random number within [0; 1).")]
//! Generate,
//! }
//!
//! async fn handle_command(
//! ctx: DispatcherHandlerCtx<Message>,
//! ) -> Result<(), RequestError> {
//! let text = match ctx.update.text() {
//! Some(text) => text,
//! None => {
//! log::info!("Received a message, but not text.");
//! return Ok(());
//! }
//! };
//!
//! let command = match Command::parse(text) {
//! Some((command, _)) => command,
//! None => {
//! log::info!("Received a text message, but not a command.");
//! return Ok(());
//! }
//! };
//!
//! match command {
//! Command::Help => ctx.answer(Command::descriptions()).send().await?,
//! Command::Generate => {
//! ctx.answer(thread_rng().gen_range(0.0, 1.0).to_string())
//! .send()
//! .await?
//! }
//! Command::Meow => ctx.answer("I am a cat! Meow!").send().await?,
//! };
//!
//! Ok(())
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! // Setup is omitted...
//! # teloxide::enable_logging!();
//! # log::info!("Starting simple_commands_bot!");
//! #
//! # let bot = Bot::from_env();
//! #
//! # Dispatcher::<RequestError>::new(bot)
//! # .message_handler(&handle_command)
//! # .dispatch()
//! # .await;
//! }
//! ```
//!
//! ## Guess a number
//! Wanna see more? This is a bot, which starts a game on each incoming message.
//! You must guess a number from 1 to 10 (inclusively):
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/dev/examples/guess_a_number_bot/src/main.rs))
//! ```rust,no_run
//! # #[macro_use]
//! # extern crate smart_default;
//! # use teloxide::prelude::*;
//! # use rand::{thread_rng, Rng};
//! // Imports are omitted...
//!
//! #[derive(SmartDefault)]
//! enum Dialogue {
//! #[default]
//! Start,
//! ReceiveAttempt(u8),
//! }
//! async fn handle_message(
//! ctx: DialogueHandlerCtx<Message, Dialogue>,
//! ) -> Result<DialogueStage<Dialogue>, RequestError> {
//! match ctx.dialogue {
//! Dialogue::Start => {
//! ctx.answer(
//! "Let's play a game! Guess a number from 1 to 10
//! (inclusively).",
//! )
//! .send()
//! .await?;
//! next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
//! }
//! Dialogue::ReceiveAttempt(secret) => match ctx.update.text() {
//! None => {
//! ctx.answer("Oh, please, send me a text message!")
//! .send()
//! .await?;
//! next(ctx.dialogue)
//! }
//! Some(text) => match text.parse::<u8>() {
//! Ok(attempt) => match attempt {
//! x if !(1..=10).contains(&x) => {
//! ctx.answer(
//! "Oh, please, send me a number in the range \
//! [1; 10]!",
//! )
//! .send()
//! .await?;
//! next(ctx.dialogue)
//! }
//! x if x == secret => {
//! ctx.answer("Congratulations! You won!")
//! .send()
//! .await?;
//! exit()
//! }
//! _ => {
//! ctx.answer("No.").send().await?;
//! next(ctx.dialogue)
//! }
//! },
//! Err(_) => {
//! ctx.answer(
//! "Oh, please, send me a number in the range [1; \
//! 10]!",
//! )
//! .send()
//! .await?;
//! next(ctx.dialogue)
//! }
//! },
//! },
//! }
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! # teloxide::enable_logging!();
//! # log::info!("Starting guess_a_number_bot!");
//! # let bot = Bot::from_env();
//! // Setup is omitted...
//!
//! Dispatcher::new(bot)
//! .message_handler(&DialogueDispatcher::new(|ctx| async move {
//! handle_message(ctx)
//! .await
//! .expect("Something wrong with the bot!")
//! }))
//! .dispatch()
//! .await;
//! }
//! ```
//!
//! Our [finite automaton], designating a user dialogue, cannot be in an invalid
//! state. See [examples/dialogue_bot] to see a bit more complicated bot with
//! dialogues.
//!
//! [See more examples](https://github.com/teloxide/teloxide/tree/dev/examples).
//!
//! ## Recommendations
//!
//! - Use this pattern:
//!
//! ```rust
//! #[tokio::main]
//! async fn main() {
//! run().await;
//! }
//!
//! async fn run() {
//! // Your logic here...
//! }
//! ```
//!
//! Instead of this:
//!
//! ```rust
//! #[tokio::main]
//! async fn main() {
//! // Your logic here...
//! }
//! ```
//!
//! The second one produces very strange compiler messages because of the
//! `#[tokio::main]` macro. However, the examples above use the second one for
//! brevity.
//!
//! [Telegram bots]: https://telegram.org/blog/bot-revolution
//! [`async`/`.await`]: https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html
//! [Rust]: https://www.rust-lang.org/
//! [finite automaton]: https://en.wikipedia.org/wiki/Finite-state_machine
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/blob/dev/examples/dialogue_bot/src/main.rs
//! [structopt]: https://docs.rs/structopt/0.3.9/structopt/
//! [@Botfather]: https://t.me/botfather
#[macro_use]
extern crate lazy_static;
#![doc(
html_logo_url = "https://github.com/teloxide/teloxide/raw/dev/logo.svg",
html_favicon_url = "https://github.com/teloxide/teloxide/raw/dev/ICON.png"
)]
#![allow(clippy::match_bool)]
mod core;
pub use bot::Bot;
pub use errors::{ApiErrorKind, DownloadError, RequestError};
mod errors;
mod net;
mod bot;
pub mod dispatching;
mod logging;
pub mod prelude;
pub mod requests;
pub mod types;
pub mod utils;
extern crate teloxide_macros;

52
src/logging.rs Normal file
View file

@ -0,0 +1,52 @@
/// Enables logging through [pretty-env-logger].
///
/// A logger will **only** print errors from teloxide and **all** logs from
/// your program.
///
/// # Example
/// ```no_compile
/// teloxide::enable_logging!();
/// ```
///
/// # Note
/// Calling this macro **is not mandatory**; you can setup if your own logger if
/// you want.
///
/// [pretty-env-logger]: https://crates.io/crates/pretty_env_logger
#[macro_export]
macro_rules! enable_logging {
() => {
teloxide::enable_logging_with_filter!(log::LevelFilter::Trace);
};
}
/// Enables logging through [pretty-env-logger] with a custom filter for your
/// program.
///
/// A logger will **only** print errors from teloxide and restrict logs from
/// your program by the specified filter.
///
/// # Example
/// Allow printing all logs from your program up to [`LevelFilter::Debug`] (i.e.
/// do not print traces):
///
/// ```no_compile
/// teloxide::enable_logging_with_filter!(log::LevelFilter::Debug);
/// ```
///
/// # Note
/// Calling this macro **is not mandatory**; you can setup if your own logger if
/// you want.
///
/// [pretty-env-logger]: https://crates.io/crates/pretty_env_logger
/// [`LevelFilter::Debug`]: https://docs.rs/log/0.4.10/log/enum.LevelFilter.html
#[macro_export]
macro_rules! enable_logging_with_filter {
($filter:expr) => {
pretty_env_logger::formatted_builder()
.write_style(pretty_env_logger::env_logger::WriteStyle::Auto)
.filter(Some(env!("CARGO_PKG_NAME")), $filter)
.filter(Some("teloxide"), log::LevelFilter::Error)
.init();
};
}

49
src/net/download.rs Normal file
View file

@ -0,0 +1,49 @@
use reqwest::Client;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::errors::DownloadError;
use super::TELEGRAM_API_URL;
pub async fn download_file<D>(
client: &Client,
token: &str,
path: &str,
destination: &mut D,
) -> Result<(), DownloadError>
where
D: AsyncWrite + Unpin,
{
let mut res = client
.get(&super::file_url(TELEGRAM_API_URL, token, path))
.send()
.await?
.error_for_status()?;
while let Some(chunk) = res.chunk().await? {
destination.write_all(&chunk).await?;
}
Ok(())
}
#[cfg(feature = "unstable-stream")]
pub async fn download_file_stream(
client: &Client,
token: &str,
path: &str,
) -> Result<impl Stream<Item = reqwest::Result<Bytes>>, reqwest::Error> {
let res = client
.get(&super::file_url(TELEGRAM_API_URL, token, path))
.send()
.await?
.error_for_status()?;
Ok(futures::stream::unfold(res, |mut res| async {
match res.chunk().await {
Err(err) => Some((Err(err), res)),
Ok(Some(c)) => Some((Ok(c), res)),
Ok(None) => None,
}
}))
}

71
src/net/mod.rs Normal file
View file

@ -0,0 +1,71 @@
#[cfg(feature = "unstable-stream")]
pub use download::download_file_stream;
pub use self::{
download::download_file,
request::{request_json, request_multipart},
telegram_response::TelegramResponse,
};
mod download;
mod request;
mod telegram_response;
const TELEGRAM_API_URL: &str = "https://api.telegram.org";
/// Creates URL for making HTTPS requests. See the [Telegram documentation].
///
/// [Telegram documentation]: https://core.telegram.org/bots/api#making-requests
fn method_url(base: &str, token: &str, method_name: &str) -> String {
format!(
"{url}/bot{token}/{method}",
url = base,
token = token,
method = method_name,
)
}
/// Creates URL for downloading a file. See the [Telegram documentation].
///
/// [Telegram documentation]: https://core.telegram.org/bots/api#file
fn file_url(base: &str, token: &str, file_path: &str) -> String {
format!(
"{url}/file/bot{token}/{file}",
url = base,
token = token,
file = file_path,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn method_url_test() {
let url = method_url(
TELEGRAM_API_URL,
"535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao",
"methodName",
);
assert_eq!(
url,
"https://api.telegram.org/bot535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao/methodName"
);
}
#[test]
fn file_url_test() {
let url = file_url(
TELEGRAM_API_URL,
"535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao",
"AgADAgADyqoxG2g8aEsu_KjjVsGF4-zetw8ABAEAAwIAA20AA_8QAwABFgQ",
);
assert_eq!(
url,
"https://api.telegram.org/file/bot535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao/AgADAgADyqoxG2g8aEsu_KjjVsGF4-zetw8ABAEAAwIAA20AA_8QAwABFgQ"
);
}
}

56
src/net/request.rs Normal file
View file

@ -0,0 +1,56 @@
use reqwest::{multipart::Form, Client, Response};
use serde::{de::DeserializeOwned, Serialize};
use crate::{requests::ResponseResult, RequestError};
use super::{TelegramResponse, TELEGRAM_API_URL};
pub async fn request_multipart<T>(
client: &Client,
token: &str,
method_name: &str,
params: Form,
) -> ResponseResult<T>
where
T: DeserializeOwned,
{
let response = client
.post(&super::method_url(TELEGRAM_API_URL, token, method_name))
.multipart(params)
.send()
.await
.map_err(RequestError::NetworkError)?;
process_response(response).await
}
pub async fn request_json<T, P>(
client: &Client,
token: &str,
method_name: &str,
params: &P,
) -> ResponseResult<T>
where
T: DeserializeOwned,
P: Serialize,
{
let response = client
.post(&super::method_url(TELEGRAM_API_URL, token, method_name))
.json(params)
.send()
.await
.map_err(RequestError::NetworkError)?;
process_response(response).await
}
async fn process_response<T>(response: Response) -> ResponseResult<T>
where
T: DeserializeOwned,
{
serde_json::from_str::<TelegramResponse<T>>(
&response.text().await.map_err(RequestError::NetworkError)?,
)
.map_err(RequestError::InvalidJson)?
.into()
}

View file

@ -0,0 +1,77 @@
use reqwest::StatusCode;
use serde::Deserialize;
use crate::{
requests::ResponseResult,
types::{False, ResponseParameters, True},
ApiErrorKind, RequestError,
};
#[derive(Deserialize)]
#[serde(untagged)]
pub enum TelegramResponse<R> {
Ok {
/// A dummy field. Used only for deserialization.
#[allow(dead_code)]
ok: True,
result: R,
},
Err {
/// A dummy field. Used only for deserialization.
#[allow(dead_code)]
ok: False,
#[serde(rename = "description")]
kind: ApiErrorKind,
error_code: u16,
response_parameters: Option<ResponseParameters>,
},
}
impl<R> Into<ResponseResult<R>> for TelegramResponse<R> {
fn into(self) -> Result<R, RequestError> {
match self {
TelegramResponse::Ok { result, .. } => Ok(result),
TelegramResponse::Err {
kind,
error_code,
response_parameters,
..
} => {
if let Some(params) = response_parameters {
match params {
ResponseParameters::RetryAfter(i) => {
Err(RequestError::RetryAfter(i))
}
ResponseParameters::MigrateToChatId(to) => {
Err(RequestError::MigrateToChatId(to))
}
}
} else {
Err(RequestError::ApiError {
kind,
status_code: StatusCode::from_u16(error_code).unwrap(),
})
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Update;
#[test]
fn terminated_by_other_get_updates() {
let expected = ApiErrorKind::TerminatedByOtherGetUpdates;
if let TelegramResponse::Err{ kind, .. } = serde_json::from_str::<TelegramResponse<Update>>(r#"{"ok":false,"error_code":409,"description":"Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"}"#).unwrap() {
assert_eq!(expected, kind);
}
else {
panic!("Этой херни здесь не должно быть");
}
}
}

14
src/prelude.rs Normal file
View file

@ -0,0 +1,14 @@
//! Commonly used items.
pub use crate::{
dispatching::{
dialogue::{
exit, next, DialogueDispatcher, DialogueHandlerCtx, DialogueStage,
GetChatId,
},
Dispatcher, DispatcherHandlerCtx, DispatcherHandlerResult,
},
requests::{Request, ResponseResult},
types::{Message, Update},
Bot, RequestError,
};

View file

@ -0,0 +1,119 @@
use crate::{
net,
requests::form_builder::FormBuilder,
types::{InputFile, MaskPosition, True},
Bot,
};
use crate::requests::{Request, ResponseResult};
use std::sync::Arc;
/// Use this method to add a new sticker to a set created by the bot.
///
/// [The official docs](https://core.telegram.org/bots/api#addstickertoset).
#[derive(Debug, Clone)]
pub struct AddStickerToSet {
bot: Arc<Bot>,
user_id: i32,
name: String,
png_sticker: InputFile,
emojis: String,
mask_position: Option<MaskPosition>,
}
#[async_trait::async_trait]
impl Request for AddStickerToSet {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"addStickerToSet",
FormBuilder::new()
.add("user_id", &self.user_id)
.await
.add("name", &self.name)
.await
.add("png_sticker", &self.png_sticker)
.await
.add("emojis", &self.emojis)
.await
.add("mask_position", &self.mask_position)
.await
.build(),
)
.await
}
}
impl AddStickerToSet {
pub(crate) fn new<N, E>(
bot: Arc<Bot>,
user_id: i32,
name: N,
png_sticker: InputFile,
emojis: E,
) -> Self
where
N: Into<String>,
E: Into<String>,
{
Self {
bot,
user_id,
name: name.into(),
png_sticker,
emojis: emojis.into(),
mask_position: None,
}
}
/// User identifier of sticker set owner.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// Sticker set name.
pub fn name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.name = val.into();
self
}
/// **Png** image with the sticker, must be up to 512 kilobytes in size,
/// dimensions must not exceed 512px, and either width or height must be
/// exactly 512px.
///
/// Pass [`InputFile::File`] to send a file that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
pub fn png_sticker(mut self, val: InputFile) -> Self {
self.png_sticker = val;
self
}
/// One or more emoji corresponding to the sticker.
pub fn emojis<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.emojis = val.into();
self
}
/// A JSON-serialized object for position where the mask should be placed on
/// faces.
pub fn mask_position(mut self, val: MaskPosition) -> Self {
self.mask_position = Some(val);
self
}
}

View file

@ -0,0 +1,115 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::True,
Bot,
};
use std::sync::Arc;
/// Use this method to send answers to callback queries sent from [inline
/// keyboards].
///
/// The answer will be displayed to the user as a notification at
/// the top of the chat screen or as an alert.
///
/// [The official docs](https://core.telegram.org/bots/api#answercallbackquery).
///
/// [inline keyboards]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct AnswerCallbackQuery {
#[serde(skip_serializing)]
bot: Arc<Bot>,
callback_query_id: String,
text: Option<String>,
show_alert: Option<bool>,
url: Option<String>,
cache_time: Option<i32>,
}
#[async_trait::async_trait]
impl Request for AnswerCallbackQuery {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"answerCallbackQuery",
&self,
)
.await
}
}
impl AnswerCallbackQuery {
pub(crate) fn new<C>(bot: Arc<Bot>, callback_query_id: C) -> Self
where
C: Into<String>,
{
let callback_query_id = callback_query_id.into();
Self {
bot,
callback_query_id,
text: None,
show_alert: None,
url: None,
cache_time: None,
}
}
/// Unique identifier for the query to be answered.
pub fn callback_query_id<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.callback_query_id = val.into();
self
}
/// Text of the notification. If not specified, nothing will be shown to the
/// user, 0-200 characters.
pub fn text<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.text = Some(val.into());
self
}
/// If `true`, an alert will be shown by the client instead of a
/// notification at the top of the chat screen. Defaults to `false`.
pub fn show_alert(mut self, val: bool) -> Self {
self.show_alert = Some(val);
self
}
/// URL that will be opened by the user's client. If you have created a
/// [`Game`] and accepted the conditions via [@Botfather], specify the
/// URL that opens your game note that this will only work if the
/// query comes from a [`callback_game`] button.
///
/// Otherwise, you may use links like `t.me/your_bot?start=XXXX` that open
/// your bot with a parameter.
///
/// [@Botfather]: https://t.me/botfather
/// [`callback_game`]: crate::types::InlineKeyboardButton
/// [`Game`]: crate::types::Game
pub fn url<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.url = Some(val.into());
self
}
/// The maximum amount of time in seconds that the result of the callback
/// query may be cached client-side. Telegram apps will support caching
/// starting in version 3.14. Defaults to 0.
pub fn cache_time(mut self, val: i32) -> Self {
self.cache_time = Some(val);
self
}
}

View file

@ -0,0 +1,157 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{InlineQueryResult, True},
Bot,
};
use std::sync::Arc;
/// Use this method to send answers to an inline query.
///
/// No more than **50** results per query are allowed.
///
/// [The official docs](https://core.telegram.org/bots/api#answerinlinequery).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct AnswerInlineQuery {
#[serde(skip_serializing)]
bot: Arc<Bot>,
inline_query_id: String,
results: Vec<InlineQueryResult>,
cache_time: Option<i32>,
is_personal: Option<bool>,
next_offset: Option<String>,
switch_pm_text: Option<String>,
switch_pm_parameter: Option<String>,
}
#[async_trait::async_trait]
impl Request for AnswerInlineQuery {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"answerInlineQuery",
&self,
)
.await
}
}
impl AnswerInlineQuery {
pub(crate) fn new<I, R>(
bot: Arc<Bot>,
inline_query_id: I,
results: R,
) -> Self
where
I: Into<String>,
R: Into<Vec<InlineQueryResult>>,
{
let inline_query_id = inline_query_id.into();
let results = results.into();
Self {
bot,
inline_query_id,
results,
cache_time: None,
is_personal: None,
next_offset: None,
switch_pm_text: None,
switch_pm_parameter: None,
}
}
/// Unique identifier for the answered query.
pub fn inline_query_id<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.inline_query_id = val.into();
self
}
/// A JSON-serialized array of results for the inline query.
pub fn results<T>(mut self, val: T) -> Self
where
T: Into<Vec<InlineQueryResult>>,
{
self.results = val.into();
self
}
/// The maximum amount of time in seconds that the result of the inline
/// query may be cached on the server.
///
/// Defaults to 300.
pub fn cache_time(mut self, val: i32) -> Self {
self.cache_time = Some(val);
self
}
/// Pass `true`, if results may be cached on the server side only for the
/// user that sent the query.
///
/// By default, results may be returned to any user who sends the same
/// query.
#[allow(clippy::wrong_self_convention)]
pub fn is_personal(mut self, val: bool) -> Self {
self.is_personal = Some(val);
self
}
/// Pass the offset that a client should send in the next query with the
/// same text to receive more results.
///
/// Pass an empty string if there are no more results or if you dont
/// support pagination. Offset length cant exceed 64 bytes.
pub fn next_offset<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.next_offset = Some(val.into());
self
}
/// If passed, clients will display a button with specified text that
/// switches the user to a private chat with the bot and sends the bot a
/// start message with the parameter [`switch_pm_parameter`].
///
/// [`switch_pm_parameter`]:
/// crate::requests::AnswerInlineQuery::switch_pm_parameter
pub fn switch_pm_text<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.switch_pm_text = Some(val.into());
self
}
/// [Deep-linking] parameter for the /start message sent to the bot when
/// user presses the switch button. 1-64 characters, only `A-Z`, `a-z`,
/// `0-9`, `_` and `-` are allowed.
///
/// Example: An inline bot that sends YouTube videos can ask the user to
/// connect the bot to their YouTube account to adapt search results
/// accordingly. To do this, it displays a Connect your YouTube account
/// button above the results, or even before showing any. The user presses
/// the button, switches to a private chat with the bot and, in doing so,
/// passes a start parameter that instructs the bot to return an oauth link.
/// Once done, the bot can offer a [`switch_inline`] button so that the user
/// can easily return to the chat where they wanted to use the bot's
/// inline capabilities.
///
/// [Deep-linking]: https://core.telegram.org/bots#deep-linking
/// [`switch_inline`]: crate::types::InlineKeyboardMarkup
pub fn switch_pm_parameter<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.switch_pm_parameter = Some(val.into());
self
}
}

View file

@ -0,0 +1,94 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::True,
Bot,
};
use std::sync::Arc;
/// Once the user has confirmed their payment and shipping details, the Bot API
/// sends the final confirmation in the form of an [`Update`] with the field
/// `pre_checkout_query`. Use this method to respond to such pre-checkout
/// queries. Note: The Bot API must receive an answer within 10 seconds after
/// the pre-checkout query was sent.
///
/// [The official docs](https://core.telegram.org/bots/api#answerprecheckoutquery).
///
/// [`Update`]: crate::types::Update
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct AnswerPreCheckoutQuery {
#[serde(skip_serializing)]
bot: Arc<Bot>,
pre_checkout_query_id: String,
ok: bool,
error_message: Option<String>,
}
#[async_trait::async_trait]
impl Request for AnswerPreCheckoutQuery {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"answerPreCheckoutQuery",
&self,
)
.await
}
}
impl AnswerPreCheckoutQuery {
pub(crate) fn new<P>(
bot: Arc<Bot>,
pre_checkout_query_id: P,
ok: bool,
) -> Self
where
P: Into<String>,
{
let pre_checkout_query_id = pre_checkout_query_id.into();
Self {
bot,
pre_checkout_query_id,
ok,
error_message: None,
}
}
/// Unique identifier for the query to be answered.
pub fn pre_checkout_query_id<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.pre_checkout_query_id = val.into();
self
}
/// Specify `true` if everything is alright (goods are available, etc.) and
/// the bot is ready to proceed with the order. Use False if there are any
/// problems.
pub fn ok(mut self, val: bool) -> Self {
self.ok = val;
self
}
/// Required if ok is `false`. Error message in human readable form that
/// explains the reason for failure to proceed with the checkout (e.g.
/// "Sorry, somebody just bought the last of our amazing black T-shirts
/// while you were busy filling out your payment details. Please choose a
/// different color or garment!").
///
/// Telegram will display this message to the user.
pub fn error_message<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.error_message = Some(val.into());
self
}
}

View file

@ -0,0 +1,99 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ShippingOption, True},
Bot,
};
use std::sync::Arc;
/// If you sent an invoice requesting a shipping address and the parameter
/// `is_flexible` was specified, the Bot API will send an [`Update`] with a
/// shipping_query field to the bot. Use this method to reply to shipping
/// queries.
///
/// [The official docs](https://core.telegram.org/bots/api#answershippingquery).
///
/// [`Update`]: crate::types::Update
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct AnswerShippingQuery {
#[serde(skip_serializing)]
bot: Arc<Bot>,
shipping_query_id: String,
ok: bool,
shipping_options: Option<Vec<ShippingOption>>,
error_message: Option<String>,
}
#[async_trait::async_trait]
impl Request for AnswerShippingQuery {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"answerShippingQuery",
&self,
)
.await
}
}
impl AnswerShippingQuery {
pub(crate) fn new<S>(bot: Arc<Bot>, shipping_query_id: S, ok: bool) -> Self
where
S: Into<String>,
{
let shipping_query_id = shipping_query_id.into();
Self {
bot,
shipping_query_id,
ok,
shipping_options: None,
error_message: None,
}
}
/// Unique identifier for the query to be answered.
pub fn shipping_query_id<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.shipping_query_id = val.into();
self
}
/// Specify `true` if delivery to the specified address is possible and
/// `false` if there are any problems (for example, if delivery to the
/// specified address is not possible).
pub fn ok(mut self, val: bool) -> Self {
self.ok = val;
self
}
/// Required if ok is `true`. A JSON-serialized array of available shipping
/// options.
pub fn shipping_options<T>(mut self, val: T) -> Self
where
T: Into<Vec<ShippingOption>>,
{
self.shipping_options = Some(val.into());
self
}
/// Required if ok is `false`. Error message in human readable form that
/// explains why it is impossible to complete the order (e.g. "Sorry,
/// delivery to your desired address is unavailable').
///
/// Telegram will display this message to the user.
pub fn error_message<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.error_message = Some(val.into());
self
}
}

View file

@ -0,0 +1,148 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{InputFile, MaskPosition, True},
Bot,
};
use std::sync::Arc;
/// Use this method to create new sticker set owned by a user. The bot will be
/// able to edit the created sticker set.
///
/// [The official docs](https://core.telegram.org/bots/api#createnewstickerset).
#[derive(Debug, Clone)]
pub struct CreateNewStickerSet {
bot: Arc<Bot>,
user_id: i32,
name: String,
title: String,
png_sticker: InputFile,
emojis: String,
contains_masks: Option<bool>,
mask_position: Option<MaskPosition>,
}
#[async_trait::async_trait]
impl Request for CreateNewStickerSet {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"createNewStickerSet",
FormBuilder::new()
.add("user_id", &self.user_id)
.await
.add("name", &self.name)
.await
.add("title", &self.title)
.await
.add("png_sticker", &self.png_sticker)
.await
.add("emojis", &self.emojis)
.await
.add("contains_masks", &self.contains_masks)
.await
.add("mask_position", &self.mask_position)
.await
.build(),
)
.await
}
}
impl CreateNewStickerSet {
pub(crate) fn new<N, T, E>(
bot: Arc<Bot>,
user_id: i32,
name: N,
title: T,
png_sticker: InputFile,
emojis: E,
) -> Self
where
N: Into<String>,
T: Into<String>,
E: Into<String>,
{
Self {
bot,
user_id,
name: name.into(),
title: title.into(),
png_sticker,
emojis: emojis.into(),
contains_masks: None,
mask_position: None,
}
}
/// User identifier of created sticker set owner.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// Short name of sticker set, to be used in `t.me/addstickers/` URLs (e.g.,
/// animals). Can contain only english letters, digits and underscores.
///
/// Must begin with a letter, can't contain consecutive underscores and must
/// end in `_by_<bot username>`. `<bot_username>` is case insensitive.
/// 1-64 characters.
pub fn name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.name = val.into();
self
}
/// Sticker set title, 1-64 characters.
pub fn title<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.title = val.into();
self
}
/// **Png** image with the sticker, must be up to 512 kilobytes in size,
/// dimensions must not exceed 512px, and either width or height must be
/// exactly 512px.
///
/// Pass [`InputFile::File`] to send a file that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
pub fn png_sticker(mut self, val: InputFile) -> Self {
self.png_sticker = val;
self
}
/// One or more emoji corresponding to the sticker.
pub fn emojis<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.emojis = val.into();
self
}
/// Pass `true`, if a set of mask stickers should be created.
pub fn contains_masks(mut self, val: bool) -> Self {
self.contains_masks = Some(val);
self
}
/// A JSON-serialized object for position where the mask should be placed on
/// faces.
pub fn mask_position(mut self, val: MaskPosition) -> Self {
self.mask_position = Some(val);
self
}
}

View file

@ -0,0 +1,57 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to delete a chat photo. Photos can't be changed for private
/// chats. The bot must be an administrator in the chat for this to work and
/// must have the appropriate admin rights.
///
/// [The official docs](https://core.telegram.org/bots/api#deletechatphoto).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DeleteChatPhoto {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for DeleteChatPhoto {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"deleteChatPhoto",
&self,
)
.await
}
}
impl DeleteChatPhoto {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,62 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to delete a group sticker set from a supergroup.
///
/// The bot must be an administrator in the chat for this to work and must have
/// the appropriate admin rights. Use the field `can_set_sticker_set` optionally
/// returned in [`Bot::get_chat`] requests to check if the bot can use this
/// method.
///
/// [The official docs](https://core.telegram.org/bots/api#deletechatstickerset).
///
/// [`Bot::get_chat`]: crate::Bot::get_chat
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DeleteChatStickerSet {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for DeleteChatStickerSet {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"deleteChatStickerSet",
&self,
)
.await
}
}
impl DeleteChatStickerSet {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target
/// supergroup (in the format `@supergroupusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,78 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to delete a message, including service messages.
///
/// The limitations are:
/// - A message can only be deleted if it was sent less than 48 hours ago.
/// - Bots can delete outgoing messages in private chats, groups, and
/// supergroups.
/// - Bots can delete incoming messages in private chats.
/// - Bots granted can_post_messages permissions can delete outgoing messages
/// in channels.
/// - If the bot is an administrator of a group, it can delete any message
/// there.
/// - If the bot has can_delete_messages permission in a supergroup or a
/// channel, it can delete any message there.
///
/// [The official docs](https://core.telegram.org/bots/api#deletemessage).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DeleteMessage {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
message_id: i32,
}
#[async_trait::async_trait]
impl Request for DeleteMessage {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"deleteMessage",
&self,
)
.await
}
}
impl DeleteMessage {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, message_id: i32) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
message_id,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Identifier of the message to delete.
pub fn message_id(mut self, val: i32) -> Self {
self.message_id = val;
self
}
}

View file

@ -0,0 +1,54 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::True,
Bot,
};
use std::sync::Arc;
/// Use this method to delete a sticker from a set created by the bot.
///
/// [The official docs](https://core.telegram.org/bots/api#deletestickerfromset).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DeleteStickerFromSet {
#[serde(skip_serializing)]
bot: Arc<Bot>,
sticker: String,
}
#[async_trait::async_trait]
impl Request for DeleteStickerFromSet {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"deleteStickerFromSet",
&self,
)
.await
}
}
impl DeleteStickerFromSet {
pub(crate) fn new<S>(bot: Arc<Bot>, sticker: S) -> Self
where
S: Into<String>,
{
let sticker = sticker.into();
Self { bot, sticker }
}
/// File identifier of the sticker.
pub fn sticker<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.sticker = val.into();
self
}
}

View file

@ -0,0 +1,44 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::True,
Bot,
};
use std::sync::Arc;
/// Use this method to remove webhook integration if you decide to switch back
/// to [Bot::get_updates].
///
/// [The official docs](https://core.telegram.org/bots/api#deletewebhook).
///
/// [Bot::get_updates]: crate::Bot::get_updates
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DeleteWebhook {
#[serde(skip_serializing)]
bot: Arc<Bot>,
}
#[async_trait::async_trait]
impl Request for DeleteWebhook {
type Output = True;
#[allow(clippy::trivially_copy_pass_by_ref)]
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"deleteWebhook",
&self,
)
.await
}
}
impl DeleteWebhook {
pub(crate) fn new(bot: Arc<Bot>) -> Self {
Self { bot }
}
}

View file

@ -0,0 +1,94 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatOrInlineMessage, InlineKeyboardMarkup, Message, ParseMode},
Bot,
};
use std::sync::Arc;
/// Use this method to edit captions of messages.
///
/// On success, if edited message is sent by the bot, the edited [`Message`] is
/// returned, otherwise [`True`] is returned.
///
/// [The official docs](https://core.telegram.org/bots/api#editmessagecaption).
///
/// [`Message`]: crate::types::Message
/// [`True`]: crate::types::True
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditMessageCaption {
#[serde(skip_serializing)]
bot: Arc<Bot>,
#[serde(flatten)]
chat_or_inline_message: ChatOrInlineMessage,
caption: Option<String>,
parse_mode: Option<ParseMode>,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for EditMessageCaption {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"editMessageCaption",
&self,
)
.await
}
}
impl EditMessageCaption {
pub(crate) fn new(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
) -> Self {
Self {
bot,
chat_or_inline_message,
caption: None,
parse_mode: None,
reply_markup: None,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// New caption of the message.
pub fn caption<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.caption = Some(val.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// A JSON-serialized object for an [inline keyboard].
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,89 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatOrInlineMessage, InlineKeyboardMarkup, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to edit live location messages.
///
/// A location can be edited until its live_period expires or editing is
/// explicitly disabled by a call to stopMessageLiveLocation. On success, if the
/// edited message was sent by the bot, the edited [`Message`] is returned,
/// otherwise [`True`] is returned.
///
/// [The official docs](https://core.telegram.org/bots/api#editmessagelivelocation).
///
/// [`Message`]: crate::types::Message
/// [`True`]: crate::types::True
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditMessageLiveLocation {
#[serde(skip_serializing)]
bot: Arc<Bot>,
#[serde(flatten)]
chat_or_inline_message: ChatOrInlineMessage,
latitude: f32,
longitude: f32,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for EditMessageLiveLocation {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"editMessageLiveLocation",
&self,
)
.await
}
}
impl EditMessageLiveLocation {
pub(crate) fn new(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
latitude: f32,
longitude: f32,
) -> Self {
Self {
bot,
chat_or_inline_message,
latitude,
longitude,
reply_markup: None,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// Latitude of new location.
pub fn latitude(mut self, val: f32) -> Self {
self.latitude = val;
self
}
/// Longitude of new location.
pub fn longitude(mut self, val: f32) -> Self {
self.longitude = val;
self
}
/// A JSON-serialized object for a new [inline keyboard].
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,102 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatOrInlineMessage, InlineKeyboardMarkup, InputMedia, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to edit animation, audio, document, photo, or video
/// messages.
///
/// If a message is a part of a message album, then it can be edited only to a
/// photo or a video. Otherwise, message type can be changed arbitrarily. When
/// inline message is edited, new file can't be uploaded. Use previously
/// uploaded file via its `file_id` or specify a URL. On success, if the edited
/// message was sent by the bot, the edited [`Message`] is returned,
/// otherwise [`True`] is returned.
///
/// [The official docs](https://core.telegram.org/bots/api#editmessagemedia).
///
/// [`Message`]: crate::types::Message
/// [`True`]: crate::types::True
#[derive(Debug, Clone)]
pub struct EditMessageMedia {
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
media: InputMedia,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for EditMessageMedia {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
let mut params = FormBuilder::new();
match &self.chat_or_inline_message {
ChatOrInlineMessage::Chat {
chat_id,
message_id,
} => {
params = params
.add("chat_id", chat_id)
.await
.add("message_id", message_id)
.await;
}
ChatOrInlineMessage::Inline { inline_message_id } => {
params =
params.add("inline_message_id", inline_message_id).await;
}
}
net::request_multipart(
self.bot.client(),
self.bot.token(),
"editMessageMedia",
params
.add("media", &self.media)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl EditMessageMedia {
pub(crate) fn new(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
media: InputMedia,
) -> Self {
Self {
bot,
chat_or_inline_message,
media,
reply_markup: None,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// A JSON-serialized object for a new media content of the message.
pub fn media(mut self, val: InputMedia) -> Self {
self.media = val;
self
}
/// A JSON-serialized object for a new [inline keyboard].
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,69 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatOrInlineMessage, InlineKeyboardMarkup, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to edit only the reply markup of messages.
///
/// On success, if edited message is sent by the bot, the edited [`Message`] is
/// returned, otherwise [`True`] is returned.
///
/// [The official docs](https://core.telegram.org/bots/api#editmessagereplymarkup).
///
/// [`Message`]: crate::types::Message
/// [`True`]: crate::types::True
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditMessageReplyMarkup {
#[serde(skip_serializing)]
bot: Arc<Bot>,
#[serde(flatten)]
chat_or_inline_message: ChatOrInlineMessage,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for EditMessageReplyMarkup {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"editMessageReplyMarkup",
&self,
)
.await
}
}
impl EditMessageReplyMarkup {
pub(crate) fn new(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
) -> Self {
Self {
bot,
chat_or_inline_message,
reply_markup: None,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// A JSON-serialized object for an [inline keyboard].
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,105 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatOrInlineMessage, InlineKeyboardMarkup, Message, ParseMode},
Bot,
};
use std::sync::Arc;
/// Use this method to edit text and game messages.
///
/// On success, if edited message is sent by the bot, the edited [`Message`] is
/// returned, otherwise [`True`] is returned.
///
/// [The official docs](https://core.telegram.org/bots/api#editmessagetext).
///
/// [`Message`]: crate::types::Message
/// [`True`]: crate::types::True
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditMessageText {
#[serde(skip_serializing)]
bot: Arc<Bot>,
#[serde(flatten)]
chat_or_inline_message: ChatOrInlineMessage,
text: String,
parse_mode: Option<ParseMode>,
disable_web_page_preview: Option<bool>,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for EditMessageText {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"editMessageText",
&self,
)
.await
}
}
impl EditMessageText {
pub(crate) fn new<T>(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
text: T,
) -> Self
where
T: Into<String>,
{
Self {
bot,
chat_or_inline_message,
text: text.into(),
parse_mode: None,
disable_web_page_preview: None,
reply_markup: None,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// New text of the message.
pub fn text<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.text = val.into();
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show [bold,
/// italic, fixed-width text or inline URLs] in your bot's message.
///
/// [Markdown]: https://core.telegram.org/bots/api#markdown-style
/// [HTML]: https://core.telegram.org/bots/api#html-style
/// [bold, italic, fixed-width text or inline URLs]: https://core.telegram.org/bots/api#formatting-options
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// Disables link previews for links in this message.
pub fn disable_web_page_preview(mut self, val: bool) -> Self {
self.disable_web_page_preview = Some(val);
self
}
/// A JSON-serialized object for an [inline keyboard].
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,72 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::ChatId,
Bot,
};
use std::sync::Arc;
/// Use this method to generate a new invite link for a chat; any previously
/// generated link is revoked.
///
/// The bot must be an administrator in the chat for this to work and must have
/// the appropriate admin rights.
///
/// ## Note
/// Each administrator in a chat generates their own invite links. Bots can't
/// use invite links generated by other administrators. If you want your bot to
/// work with invite links, it will need to generate its own link using
/// [`Bot::export_chat_invite_link`] after this the link will become available
/// to the bot via the [`Bot::get_chat`] method. If your bot needs to generate a
/// new invite link replacing its previous one, use
/// [`Bot::export_chat_invite_link`] again.
///
/// [The official docs](https://core.telegram.org/bots/api#exportchatinvitelink).
///
/// [`Bot::export_chat_invite_link`]: crate::Bot::export_chat_invite_link
/// [`Bot::get_chat`]: crate::Bot::get_chat
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct ExportChatInviteLink {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for ExportChatInviteLink {
type Output = String;
/// Returns the new invite link as `String` on success.
async fn send(&self) -> ResponseResult<String> {
net::request_json(
self.bot.client(),
self.bot.token(),
"exportChatInviteLink",
&self,
)
.await
}
}
impl ExportChatInviteLink {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,99 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to forward messages of any kind.
///
/// [`The official docs`](https://core.telegram.org/bots/api#forwardmessage).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct ForwardMessage {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
from_chat_id: ChatId,
disable_notification: Option<bool>,
message_id: i32,
}
#[async_trait::async_trait]
impl Request for ForwardMessage {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"forwardMessage",
&self,
)
.await
}
}
impl ForwardMessage {
pub(crate) fn new<C, F>(
bot: Arc<Bot>,
chat_id: C,
from_chat_id: F,
message_id: i32,
) -> Self
where
C: Into<ChatId>,
F: Into<ChatId>,
{
let chat_id = chat_id.into();
let from_chat_id = from_chat_id.into();
Self {
bot,
chat_id,
from_chat_id,
message_id,
disable_notification: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Unique identifier for the chat where the original message was sent (or
/// channel username in the format `@channelusername`).
#[allow(clippy::wrong_self_convention)]
pub fn from_chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.from_chat_id = val.into();
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// Message identifier in the chat specified in [`from_chat_id`].
///
/// [`from_chat_id`]: ForwardMessage::from_chat_id
pub fn message_id(mut self, val: i32) -> Self {
self.message_id = val;
self
}
}

View file

@ -0,0 +1,52 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{Chat, ChatId},
Bot,
};
use std::sync::Arc;
/// Use this method to get up to date information about the chat (current name
/// of the user for one-on-one conversations, current username of a user, group
/// or channel, etc.).
///
/// [The official docs](https://core.telegram.org/bots/api#getchat).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetChat {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for GetChat {
type Output = Chat;
async fn send(&self) -> ResponseResult<Chat> {
net::request_json(self.bot.client(), self.bot.token(), "getChat", &self)
.await
}
}
impl GetChat {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,60 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, ChatMember},
Bot,
};
use std::sync::Arc;
/// Use this method to get a list of administrators in a chat.
///
/// If the chat is a group or a supergroup and no administrators were appointed,
/// only the creator will be returned.
///
/// [The official docs](https://core.telegram.org/bots/api#getchatadministrators).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetChatAdministrators {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for GetChatAdministrators {
type Output = Vec<ChatMember>;
/// On success, returns an array that contains information about all chat
/// administrators except other bots.
async fn send(&self) -> ResponseResult<Vec<ChatMember>> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getChatAdministrators",
&self,
)
.await
}
}
impl GetChatAdministrators {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,66 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, ChatMember},
Bot,
};
use std::sync::Arc;
/// Use this method to get information about a member of a chat.
///
/// [The official docs](https://core.telegram.org/bots/api#getchatmember).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetChatMember {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
user_id: i32,
}
#[async_trait::async_trait]
impl Request for GetChatMember {
type Output = ChatMember;
async fn send(&self) -> ResponseResult<ChatMember> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getChatMember",
&self,
)
.await
}
}
impl GetChatMember {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, user_id: i32) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
}
}
/// Unique identifier for the target chat or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Unique identifier of the target user.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
}

View file

@ -0,0 +1,55 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::ChatId,
Bot,
};
use std::sync::Arc;
/// Use this method to get the number of members in a chat.
///
/// [The official docs](https://core.telegram.org/bots/api#getchatmemberscount).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetChatMembersCount {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for GetChatMembersCount {
type Output = i32;
async fn send(&self) -> ResponseResult<i32> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getChatMembersCount",
&self,
)
.await
}
}
impl GetChatMembersCount {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

View file

@ -0,0 +1,67 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::File,
Bot,
};
use std::sync::Arc;
/// Use this method to get basic info about a file and prepare it for
/// downloading.
///
/// For the moment, bots can download files of up to `20MB` in size.
///
/// The file can then be downloaded via the link
/// `https://api.telegram.org/file/bot<token>/<file_path>`, where `<file_path>`
/// is taken from the response. It is guaranteed that the link will be valid
/// for at least `1` hour. When the link expires, a new one can be requested by
/// calling [`GetFile`] again.
///
/// **Note**: This function may not preserve the original file name and MIME
/// type. You should save the file's MIME type and name (if available) when the
/// [`File`] object is received.
///
/// [The official docs](https://core.telegram.org/bots/api#getfile).
///
/// [`File`]: crate::types::file
/// [`GetFile`]: self::GetFile
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetFile {
#[serde(skip_serializing)]
bot: Arc<Bot>,
file_id: String,
}
#[async_trait::async_trait]
impl Request for GetFile {
type Output = File;
async fn send(&self) -> ResponseResult<File> {
net::request_json(self.bot.client(), self.bot.token(), "getFile", &self)
.await
}
}
impl GetFile {
pub(crate) fn new<F>(bot: Arc<Bot>, file_id: F) -> Self
where
F: Into<String>,
{
Self {
bot,
file_id: file_id.into(),
}
}
/// File identifier to get info about.
pub fn file_id<F>(mut self, value: F) -> Self
where
F: Into<String>,
{
self.file_id = value.into();
self
}
}

View file

@ -0,0 +1,71 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatOrInlineMessage, GameHighScore},
Bot,
};
use std::sync::Arc;
/// Use this method to get data for high score tables.
///
/// Will return the score of the specified user and several of his neighbors in
/// a game.
///
/// ## Note
/// This method will currently return scores for the target user, plus two of
/// his closest neighbors on each side. Will also return the top three users if
/// the user and his neighbors are not among them. Please note that this
/// behavior is subject to change.
///
/// [The official docs](https://core.telegram.org/bots/api#getgamehighscores).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetGameHighScores {
#[serde(skip_serializing)]
bot: Arc<Bot>,
#[serde(flatten)]
chat_or_inline_message: ChatOrInlineMessage,
user_id: i32,
}
#[async_trait::async_trait]
impl Request for GetGameHighScores {
type Output = Vec<GameHighScore>;
async fn send(&self) -> ResponseResult<Vec<GameHighScore>> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getGameHighScores",
&self,
)
.await
}
}
impl GetGameHighScores {
pub(crate) fn new(
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
user_id: i32,
) -> Self {
Self {
bot,
chat_or_inline_message,
user_id,
}
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {
self.chat_or_inline_message = val;
self
}
/// Target user id.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
}

View file

@ -0,0 +1,35 @@
use crate::{
net,
requests::{Request, ResponseResult},
types::Me,
Bot,
};
use serde::Serialize;
use std::sync::Arc;
/// A simple method for testing your bot's auth token. Requires no parameters.
///
/// [The official docs](https://core.telegram.org/bots/api#getme).
#[derive(Debug, Clone, Serialize)]
pub struct GetMe {
#[serde(skip_serializing)]
bot: Arc<Bot>,
}
#[async_trait::async_trait]
impl Request for GetMe {
type Output = Me;
/// Returns basic information about the bot.
#[allow(clippy::trivially_copy_pass_by_ref)]
async fn send(&self) -> ResponseResult<Me> {
net::request_json(self.bot.client(), self.bot.token(), "getMe", &self)
.await
}
}
impl GetMe {
pub(crate) fn new(bot: Arc<Bot>) -> Self {
Self { bot }
}
}

View file

@ -0,0 +1,54 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::StickerSet,
Bot,
};
use std::sync::Arc;
/// Use this method to get a sticker set.
///
/// [The official docs](https://core.telegram.org/bots/api#getstickerset).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetStickerSet {
#[serde(skip_serializing)]
bot: Arc<Bot>,
name: String,
}
#[async_trait::async_trait]
impl Request for GetStickerSet {
type Output = StickerSet;
async fn send(&self) -> ResponseResult<StickerSet> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getStickerSet",
&self,
)
.await
}
}
impl GetStickerSet {
pub(crate) fn new<N>(bot: Arc<Bot>, name: N) -> Self
where
N: Into<String>,
{
let name = name.into();
Self { bot, name }
}
/// Name of the sticker set.
pub fn name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.name = val.into();
self
}
}

View file

@ -0,0 +1,139 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{AllowedUpdate, Update},
Bot, RequestError,
};
use serde_json::Value;
use std::sync::Arc;
/// Use this method to receive incoming updates using long polling ([wiki]).
///
/// **Notes:**
/// 1. This method will not work if an outgoing webhook is set up.
/// 2. In order to avoid getting duplicate updates,
/// recalculate offset after each server response.
///
/// [The official docs](https://core.telegram.org/bots/api#getupdates).
///
/// [wiki]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetUpdates {
#[serde(skip_serializing)]
bot: Arc<Bot>,
pub(crate) offset: Option<i32>,
pub(crate) limit: Option<u8>,
pub(crate) timeout: Option<u32>,
pub(crate) allowed_updates: Option<Vec<AllowedUpdate>>,
}
#[async_trait::async_trait]
impl Request for GetUpdates {
type Output = Vec<Result<Update, (Value, serde_json::Error)>>;
/// Deserialize to `Vec<serde_json::Result<Update>>` instead of
/// `Vec<Update>`, because we want to parse the rest of updates even if our
/// library hasn't parsed one.
async fn send(
&self,
) -> ResponseResult<Vec<Result<Update, (Value, serde_json::Error)>>> {
let value: Value = net::request_json(
self.bot.client(),
self.bot.token(),
"getUpdates",
&self,
)
.await?;
match value {
Value::Array(array) => Ok(array
.into_iter()
.map(|value| {
serde_json::from_str(&value.to_string())
.map_err(|error| (value, error))
})
.collect()),
_ => Err(RequestError::InvalidJson(
serde_json::from_value::<Vec<Update>>(value)
.expect_err("get_update must return Value::Array"),
)),
}
}
}
impl GetUpdates {
pub(crate) fn new(bot: Arc<Bot>) -> Self {
Self {
bot,
offset: None,
limit: None,
timeout: None,
allowed_updates: None,
}
}
/// Identifier of the first update to be returned.
///
/// Must be greater by one than the highest among the identifiers of
/// previously received updates. By default, updates starting with the
/// earliest unconfirmed update are returned. An update is considered
/// confirmed as soon as [`GetUpdates`] is called with an [`offset`]
/// higher than its [`id`]. The negative offset can be specified to
/// retrieve updates starting from `-offset` update from the end of the
/// updates queue. All previous updates will forgotten.
///
/// [`GetUpdates`]: self::GetUpdates
/// [`offset`]: self::GetUpdates::offset
/// [`id`]: crate::types::Update::id
pub fn offset(mut self, value: i32) -> Self {
self.offset = Some(value);
self
}
/// Limits the number of updates to be retrieved.
///
/// Values between `1`—`100` are accepted. Defaults to `100`.
pub fn limit(mut self, value: u8) -> Self {
self.limit = Some(value);
self
}
/// Timeout in seconds for long polling.
///
/// Defaults to `0`, i.e. usual short polling. Should be positive, short
/// polling should be used for testing purposes only.
pub fn timeout(mut self, value: u32) -> Self {
self.timeout = Some(value);
self
}
/// List the types of updates you want your bot to receive.
///
/// For example, specify [[`Message`], [`EditedChannelPost`],
/// [`CallbackQuery`]] to only receive updates of these types.
/// See [`AllowedUpdate`] for a complete list of available update types.
///
/// Specify an empty list to receive all updates regardless of type
/// (default). If not specified, the previous setting will be used.
///
/// **Note:**
/// This parameter doesn't affect updates created before the call to the
/// [`Bot::get_updates`], so unwanted updates may be received for a short
/// period of time.
///
/// [`Message`]: self::AllowedUpdate::Message
/// [`EditedChannelPost`]: self::AllowedUpdate::EditedChannelPost
/// [`CallbackQuery`]: self::AllowedUpdate::CallbackQuery
/// [`AllowedUpdate`]: self::AllowedUpdate
/// [`Bot::get_updates`]: crate::Bot::get_updates
pub fn allowed_updates<T>(mut self, value: T) -> Self
where
T: Into<Vec<AllowedUpdate>>,
{
self.allowed_updates = Some(value.into());
self
}
}

View file

@ -0,0 +1,70 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::UserProfilePhotos,
Bot,
};
use std::sync::Arc;
/// Use this method to get a list of profile pictures for a user.
///
/// [The official docs](https://core.telegram.org/bots/api#getuserprofilephotos).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct GetUserProfilePhotos {
#[serde(skip_serializing)]
bot: Arc<Bot>,
user_id: i32,
offset: Option<i32>,
limit: Option<i32>,
}
#[async_trait::async_trait]
impl Request for GetUserProfilePhotos {
type Output = UserProfilePhotos;
async fn send(&self) -> ResponseResult<UserProfilePhotos> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getUserProfilePhotos",
&self,
)
.await
}
}
impl GetUserProfilePhotos {
pub(crate) fn new(bot: Arc<Bot>, user_id: i32) -> Self {
Self {
bot,
user_id,
offset: None,
limit: None,
}
}
/// Unique identifier of the target user.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// Sequential number of the first photo to be returned. By default, all
/// photos are returned.
pub fn offset(mut self, val: i32) -> Self {
self.offset = Some(val);
self
}
/// Limits the number of photos to be retrieved. Values between 1—100 are
/// accepted.
///
/// Defaults to 100.
pub fn limit(mut self, val: i32) -> Self {
self.limit = Some(val);
self
}
}

View file

@ -0,0 +1,45 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::WebhookInfo,
Bot,
};
use std::sync::Arc;
/// Use this method to get current webhook status.
///
/// If the bot is using [`Bot::get_updates`], will return an object with the url
/// field empty.
///
/// [The official docs](https://core.telegram.org/bots/api#getwebhookinfo).
///
/// [`Bot::get_updates`]: crate::Bot::get_updates
#[derive(Debug, Clone, Serialize)]
pub struct GetWebhookInfo {
#[serde(skip_serializing)]
bot: Arc<Bot>,
}
#[async_trait::async_trait]
impl Request for GetWebhookInfo {
type Output = WebhookInfo;
#[allow(clippy::trivially_copy_pass_by_ref)]
async fn send(&self) -> ResponseResult<WebhookInfo> {
net::request_json(
self.bot.client(),
self.bot.token(),
"getWebhookInfo",
&self,
)
.await
}
}
impl GetWebhookInfo {
pub(crate) fn new(bot: Arc<Bot>) -> Self {
Self { bot }
}
}

View file

@ -0,0 +1,84 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to kick a user from a group, a supergroup or a channel.
///
/// In the case of supergroups and channels, the user will not be able to return
/// to the group on their own using invite links, etc., unless [unbanned] first.
/// The bot must be an administrator in the chat for this to work and must have
/// the appropriate admin rights.
///
/// [The official docs](https://core.telegram.org/bots/api#kickchatmember).
///
/// [unbanned]: crate::Bot::unban_chat_member
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct KickChatMember {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
user_id: i32,
until_date: Option<i32>,
}
#[async_trait::async_trait]
impl Request for KickChatMember {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"kickChatMember",
&self,
)
.await
}
}
impl KickChatMember {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, user_id: i32) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
until_date: None,
}
}
/// Unique identifier for the target group or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Unique identifier of the target user.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// Date when the user will be unbanned, unix time.
///
/// If user is banned for more than 366 days or less than 30 seconds from
/// the current time they are considered to be banned forever.
pub fn until_date(mut self, val: i32) -> Self {
self.until_date = Some(val);
self
}
}

View file

@ -0,0 +1,55 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method for your bot to leave a group, supergroup or channel.
///
/// [The official docs](https://core.telegram.org/bots/api#leavechat).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct LeaveChat {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
}
#[async_trait::async_trait]
impl Request for LeaveChat {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"leaveChat",
&self,
)
.await
}
}
impl LeaveChat {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self { bot, chat_id }
}
/// Unique identifier for the target chat or username of the target
/// supergroup or channel (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
}

132
src/requests/all/mod.rs Normal file
View file

@ -0,0 +1,132 @@
mod add_sticker_to_set;
mod answer_callback_query;
mod answer_inline_query;
mod answer_pre_checkout_query;
mod answer_shipping_query;
mod create_new_sticker_set;
mod delete_chat_photo;
mod delete_chat_sticker_set;
mod delete_message;
mod delete_sticker_from_set;
mod delete_webhook;
mod edit_message_caption;
mod edit_message_live_location;
mod edit_message_media;
mod edit_message_reply_markup;
mod edit_message_text;
mod export_chat_invite_link;
mod forward_message;
mod get_chat;
mod get_chat_administrators;
mod get_chat_member;
mod get_chat_members_count;
mod get_file;
mod get_game_high_scores;
mod get_me;
mod get_sticker_set;
mod get_updates;
mod get_user_profile_photos;
mod get_webhook_info;
mod kick_chat_member;
mod leave_chat;
mod pin_chat_message;
mod promote_chat_member;
mod restrict_chat_member;
mod send_animation;
mod send_audio;
mod send_chat_action;
mod send_contact;
mod send_document;
mod send_game;
mod send_invoice;
mod send_location;
mod send_media_group;
mod send_message;
mod send_photo;
mod send_poll;
mod send_sticker;
mod send_venue;
mod send_video;
mod send_video_note;
mod send_voice;
mod set_chat_administrator_custom_title;
mod set_chat_description;
mod set_chat_permissions;
mod set_chat_photo;
mod set_chat_sticker_set;
mod set_chat_title;
mod set_game_score;
mod set_sticker_position_in_set;
mod set_webhook;
mod stop_message_live_location;
mod stop_poll;
mod unban_chat_member;
mod unpin_chat_message;
mod upload_sticker_file;
pub use add_sticker_to_set::*;
pub use answer_callback_query::*;
pub use answer_inline_query::*;
pub use answer_pre_checkout_query::*;
pub use answer_shipping_query::*;
pub use create_new_sticker_set::*;
pub use delete_chat_photo::*;
pub use delete_chat_sticker_set::*;
pub use delete_message::*;
pub use delete_sticker_from_set::*;
pub use delete_webhook::*;
pub use edit_message_caption::*;
pub use edit_message_live_location::*;
pub use edit_message_media::*;
pub use edit_message_reply_markup::*;
pub use edit_message_text::*;
pub use export_chat_invite_link::*;
pub use forward_message::*;
pub use get_chat::*;
pub use get_chat_administrators::*;
pub use get_chat_member::*;
pub use get_chat_members_count::*;
pub use get_file::*;
pub use get_game_high_scores::*;
pub use get_me::*;
pub use get_sticker_set::*;
pub use get_updates::*;
pub use get_user_profile_photos::*;
pub use get_webhook_info::*;
pub use kick_chat_member::*;
pub use leave_chat::*;
pub use pin_chat_message::*;
pub use promote_chat_member::*;
pub use restrict_chat_member::*;
pub use send_animation::*;
pub use send_audio::*;
pub use send_chat_action::*;
pub use send_contact::*;
pub use send_document::*;
pub use send_game::*;
pub use send_invoice::*;
pub use send_location::*;
pub use send_media_group::*;
pub use send_message::*;
pub use send_photo::*;
pub use send_poll::*;
pub use send_sticker::*;
pub use send_venue::*;
pub use send_video::*;
pub use send_video_note::*;
pub use send_voice::*;
pub use set_chat_administrator_custom_title::*;
pub use set_chat_description::*;
pub use set_chat_permissions::*;
pub use set_chat_photo::*;
pub use set_chat_sticker_set::*;
pub use set_chat_title::*;
pub use set_game_score::*;
pub use set_sticker_position_in_set::*;
pub use set_webhook::*;
pub use std::pin::Pin;
pub use stop_message_live_location::*;
pub use stop_poll::*;
pub use unban_chat_member::*;
pub use unpin_chat_message::*;
pub use upload_sticker_file::*;

View file

@ -0,0 +1,81 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to pin a message in a group, a supergroup, or a channel.
///
/// The bot must be an administrator in the chat for this to work and must have
/// the `can_pin_messages` admin right in the supergroup or `can_edit_messages`
/// admin right in the channel.
///
/// [The official docs](https://core.telegram.org/bots/api#pinchatmessage).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct PinChatMessage {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
message_id: i32,
disable_notification: Option<bool>,
}
#[async_trait::async_trait]
impl Request for PinChatMessage {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"pinChatMessage",
&self,
)
.await
}
}
impl PinChatMessage {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, message_id: i32) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
message_id,
disable_notification: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Identifier of a message to pin.
pub fn message_id(mut self, val: i32) -> Self {
self.message_id = val;
self
}
/// Pass `true`, if it is not necessary to send a notification to all chat
/// members about the new pinned message.
///
/// Notifications are always disabled in channels.
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
}

View file

@ -0,0 +1,141 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method to promote or demote a user in a supergroup or a channel.
///
/// The bot must be an administrator in the chat for this to work and must have
/// the appropriate admin rights. Pass False for all boolean parameters to
/// demote a user.
///
/// [The official docs](https://core.telegram.org/bots/api#promotechatmember).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct PromoteChatMember {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
user_id: i32,
can_change_info: Option<bool>,
can_post_messages: Option<bool>,
can_edit_messages: Option<bool>,
can_delete_messages: Option<bool>,
can_invite_users: Option<bool>,
can_restrict_members: Option<bool>,
can_pin_messages: Option<bool>,
can_promote_members: Option<bool>,
}
#[async_trait::async_trait]
impl Request for PromoteChatMember {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"promoteChatMember",
&self,
)
.await
}
}
impl PromoteChatMember {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, user_id: i32) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
can_change_info: None,
can_post_messages: None,
can_edit_messages: None,
can_delete_messages: None,
can_invite_users: None,
can_restrict_members: None,
can_pin_messages: None,
can_promote_members: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Unique identifier of the target user.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// Pass `true`, if the administrator can change chat title, photo and other
/// settings.
pub fn can_change_info(mut self, val: bool) -> Self {
self.can_change_info = Some(val);
self
}
/// Pass `true`, if the administrator can create channel posts, channels
/// only.
pub fn can_post_messages(mut self, val: bool) -> Self {
self.can_post_messages = Some(val);
self
}
/// Pass `true`, if the administrator can edit messages of other users and
/// can pin messages, channels only.
pub fn can_edit_messages(mut self, val: bool) -> Self {
self.can_edit_messages = Some(val);
self
}
/// Pass `true`, if the administrator can delete messages of other users.
pub fn can_delete_messages(mut self, val: bool) -> Self {
self.can_delete_messages = Some(val);
self
}
/// Pass `true`, if the administrator can invite new users to the chat.
pub fn can_invite_users(mut self, val: bool) -> Self {
self.can_invite_users = Some(val);
self
}
/// Pass `true`, if the administrator can restrict, ban or unban chat
/// members.
pub fn can_restrict_members(mut self, val: bool) -> Self {
self.can_restrict_members = Some(val);
self
}
/// Pass `true`, if the administrator can pin messages, supergroups only.
pub fn can_pin_messages(mut self, val: bool) -> Self {
self.can_pin_messages = Some(val);
self
}
/// Pass `true`, if the administrator can add new administrators with a
/// subset of his own privileges or demote administrators that he has
/// promoted, directly or indirectly (promoted by administrators that were
/// appointed by him).
pub fn can_promote_members(mut self, val: bool) -> Self {
self.can_promote_members = Some(val);
self
}
}

View file

@ -0,0 +1,94 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, ChatPermissions, True},
Bot,
};
use std::sync::Arc;
/// Use this method to restrict a user in a supergroup.
///
/// The bot must be an administrator in the supergroup for this to work and must
/// have the appropriate admin rights. Pass `true` for all permissions to lift
/// restrictions from a user.
///
/// [The official docs](https://core.telegram.org/bots/api#restrictchatmember).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct RestrictChatMember {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
user_id: i32,
permissions: ChatPermissions,
until_date: Option<i32>,
}
#[async_trait::async_trait]
impl Request for RestrictChatMember {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"restrictChatMember",
&self,
)
.await
}
}
impl RestrictChatMember {
pub(crate) fn new<C>(
bot: Arc<Bot>,
chat_id: C,
user_id: i32,
permissions: ChatPermissions,
) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
permissions,
until_date: None,
}
}
/// Unique identifier for the target chat or username of the target
/// supergroup (in the format `@supergroupusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Unique identifier of the target user.
pub fn user_id(mut self, val: i32) -> Self {
self.user_id = val;
self
}
/// New user permissions.
pub fn permissions(mut self, val: ChatPermissions) -> Self {
self.permissions = val;
self
}
/// Date when restrictions will be lifted for the user, unix time.
///
/// If user is restricted for more than 366 days or less than 30 seconds
/// from the current time, they are considered to be restricted forever.
pub fn until_date(mut self, val: i32) -> Self {
self.until_date = Some(val);
self
}
}

View file

@ -0,0 +1,188 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send animation files (GIF or H.264/MPEG-4 AVC video
/// without sound).
///
/// Bots can currently send animation files of up to 50 MB in size, this limit
/// may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#sendanimation).
#[derive(Debug, Clone)]
pub struct SendAnimation {
bot: Arc<Bot>,
pub chat_id: ChatId,
pub animation: InputFile,
pub duration: Option<u32>,
pub width: Option<u32>,
pub height: Option<u32>,
pub thumb: Option<InputFile>,
pub caption: Option<String>,
pub parse_mode: Option<ParseMode>,
pub disable_notification: Option<bool>,
pub reply_to_message_id: Option<i32>,
pub reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendAnimation {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendAnimation",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("animation", &self.animation)
.await
.add("duration", &self.duration)
.await
.add("width", &self.width)
.await
.add("height", &self.height)
.await
.add("thumb", &self.thumb)
.await
.add("caption", &self.caption)
.await
.add("parse_mode", &self.parse_mode)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendAnimation {
pub(crate) fn new<C>(
bot: Arc<Bot>,
chat_id: C,
animation: InputFile,
) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
animation,
duration: None,
width: None,
height: None,
thumb: None,
caption: None,
parse_mode: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, value: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = value.into();
self
}
/// Animation to send.
pub fn animation(mut self, val: InputFile) -> Self {
self.animation = val;
self
}
/// Duration of sent animation in seconds.
pub fn duration(mut self, value: u32) -> Self {
self.duration = Some(value);
self
}
/// Animation width.
pub fn width(mut self, value: u32) -> Self {
self.width = Some(value);
self
}
/// Animation height.
pub fn height(mut self, value: u32) -> Self {
self.height = Some(value);
self
}
/// Thumbnail of the file sent; can be ignored if thumbnail generation for
/// the file is supported server-side.
///
/// The thumbnail should be in JPEG format and less than 200 kB in size. A
/// thumbnails width and height should not exceed 320. Ignored if the
/// file is not uploaded using [`InputFile::File`]. Thumbnails cant be
/// reused and can be only uploaded as a new file, with
/// [`InputFile::File`].
///
/// [`InputFile::File`]: crate::types::InputFile::File
pub fn thumb(mut self, value: InputFile) -> Self {
self.thumb = Some(value);
self
}
/// Animation caption, `0`-`1024` characters.
pub fn caption<T>(mut self, value: T) -> Self
where
T: Into<String>,
{
self.caption = Some(value.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, value: ParseMode) -> Self {
self.parse_mode = Some(value);
self
}
/// Sends the message silently. Users will receive a notification with no
/// sound.
pub fn disable_notification(mut self, value: bool) -> Self {
self.disable_notification = Some(value);
self
}
/// If the message is a reply, [id] of the original message.
///
/// [id]: crate::types::Message::id
pub fn reply_to_message_id(mut self, value: i32) -> Self {
self.reply_to_message_id = Some(value);
self
}
/// Additional interface options.
pub fn reply_markup<T>(mut self, value: T) -> Self
where
T: Into<ReplyMarkup>,
{
self.reply_markup = Some(value.into());
self
}
}

View file

@ -0,0 +1,209 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send audio files, if you want Telegram clients to display
/// them in the music player.
///
/// Your audio must be in the .MP3 or .M4A format. Bots can currently send audio
/// files of up to 50 MB in size, this limit may be changed in the future.
///
/// For sending voice messages, use the [`Bot::send_voice`] method instead.
///
/// [The official docs](https://core.telegram.org/bots/api#sendaudio).
///
/// [`Bot::send_voice`]: crate::Bot::send_voice
#[derive(Debug, Clone)]
pub struct SendAudio {
bot: Arc<Bot>,
chat_id: ChatId,
audio: InputFile,
caption: Option<String>,
parse_mode: Option<ParseMode>,
duration: Option<i32>,
performer: Option<String>,
title: Option<String>,
thumb: Option<InputFile>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendAudio {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendAudio",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("audio", &self.audio)
.await
.add("caption", &self.caption)
.await
.add("parse_mode", &self.parse_mode)
.await
.add("duration", &self.duration)
.await
.add("performer", &self.performer)
.await
.add("title", &self.title)
.await
.add("thumb", &self.thumb)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendAudio {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, audio: InputFile) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
audio,
caption: None,
parse_mode: None,
duration: None,
performer: None,
title: None,
thumb: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Audio file to send.
///
/// Pass [`InputFile::File`] to send a file that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn audio(mut self, val: InputFile) -> Self {
self.audio = val;
self
}
/// Audio caption, 0-1024 characters.
pub fn caption<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.caption = Some(val.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// Duration of the audio in seconds.
pub fn duration(mut self, val: i32) -> Self {
self.duration = Some(val);
self
}
/// Performer.
pub fn performer<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.performer = Some(val.into());
self
}
/// Track name.
pub fn title<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.title = Some(val.into());
self
}
/// Thumbnail of the file sent; can be ignored if thumbnail generation for
/// the file is supported server-side.
///
/// The thumbnail should be in JPEG format and less than 200 kB in size. A
/// thumbnails width and height should not exceed 320. Ignored if the
/// file is not uploaded using `multipart/form-data`. Thumbnails cant
/// be reused and can be only uploaded as a new file, so you can pass
/// `attach://<file_attach_name>` if the thumbnail was uploaded using
/// `multipart/form-data` under `<file_attach_name>`. [More info on
/// Sending Files »].
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn thumb(mut self, val: InputFile) -> Self {
self.thumb = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options. A JSON-serialized object for an [inline
/// keyboard], [custom reply keyboard], instructions to remove reply
/// keyboard or to force a reply from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,120 @@
use serde::{Deserialize, Serialize};
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, True},
Bot,
};
use std::sync::Arc;
/// Use this method when you need to tell the user that something is happening
/// on the bot's side.
///
/// The status is set for 5 seconds or less (when a message arrives from your
/// bot, Telegram clients clear its typing status).
///
/// ## Note
/// Example: The [ImageBot] needs some time to process a request and upload the
/// image. Instead of sending a text message along the lines of “Retrieving
/// image, please wait…”, the bot may use [`Bot::send_chat_action`] with `action
/// = upload_photo`. The user will see a `sending photo` status for the bot.
///
/// We only recommend using this method when a response from the bot will take a
/// **noticeable** amount of time to arrive.
///
/// [ImageBot]: https://t.me/imagebot
/// [`Bot::send_chat_action`]: crate::Bot::send_chat_action
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendChatAction {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
action: SendChatActionKind,
}
/// A type of action used in [`SendChatAction`].
///
/// [`SendChatAction`]: crate::requests::SendChatAction
#[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SendChatActionKind {
/// For [text messages](crate::Bot::send_message).
Typing,
/// For [photos](crate::Bot::send_photo).
UploadPhoto,
/// For [videos](crate::Bot::send_video).
RecordVideo,
/// For [videos](crate::Bot::send_video).
UploadVideo,
/// For [audio files](crate::Bot::send_audio).
RecordAudio,
/// For [audio files](crate::Bot::send_audio).
UploadAudio,
/// For [general files](crate::Bot::send_document).
UploadDocument,
/// For [location data](crate::Bot::send_location).
FindLocation,
/// For [video notes](crate::Bot::send_video_note).
RecordVideoNote,
/// For [video notes](crate::Bot::send_video_note).
UploadVideoNote,
}
#[async_trait::async_trait]
impl Request for SendChatAction {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendChatAction",
&self,
)
.await
}
}
impl SendChatAction {
pub(crate) fn new<C>(
bot: Arc<Bot>,
chat_id: C,
action: SendChatActionKind,
) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
action,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Type of action to broadcast.
pub fn action(mut self, val: SendChatActionKind) -> Self {
self.action = val;
self
}
}

View file

@ -0,0 +1,141 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send phone contacts.
///
/// [The official docs](https://core.telegram.org/bots/api#sendcontact).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendContact {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
phone_number: String,
first_name: String,
last_name: Option<String>,
vcard: Option<String>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendContact {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendContact",
&self,
)
.await
}
}
impl SendContact {
pub(crate) fn new<C, P, F>(
bot: Arc<Bot>,
chat_id: C,
phone_number: P,
first_name: F,
) -> Self
where
C: Into<ChatId>,
P: Into<String>,
F: Into<String>,
{
let chat_id = chat_id.into();
let phone_number = phone_number.into();
let first_name = first_name.into();
Self {
bot,
chat_id,
phone_number,
first_name,
last_name: None,
vcard: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Contact's phone number.
pub fn phone_number<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.phone_number = val.into();
self
}
/// Contact's first name.
pub fn first_name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.first_name = val.into();
self
}
/// Contact's last name.
pub fn last_name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.last_name = Some(val.into());
self
}
/// Additional data about the contact in the form of a [vCard], 0-2048
/// bytes.
///
/// [vCard]: https://en.wikipedia.org/wiki/VCard
pub fn vcard<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.vcard = Some(val.into());
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,160 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send general files.
///
/// Bots can currently send files of any type of up to 50 MB in size, this limit
/// may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#senddocument).
#[derive(Debug, Clone)]
pub struct SendDocument {
bot: Arc<Bot>,
chat_id: ChatId,
document: InputFile,
thumb: Option<InputFile>,
caption: Option<String>,
parse_mode: Option<ParseMode>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendDocument {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendDocument",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("document", &self.document)
.await
.add("thumb", &self.thumb)
.await
.add("caption", &self.caption)
.await
.add("parse_mode", &self.parse_mode)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendDocument {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, document: InputFile) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
document,
thumb: None,
caption: None,
parse_mode: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// File to send.
///
/// Pass a file_id as String to send a file that exists on the
/// Telegram servers (recommended), pass an HTTP URL as a String for
/// Telegram to get a file from the Internet, or upload a new one using
/// `multipart/form-data`. [More info on Sending Files »].
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn document(mut self, val: InputFile) -> Self {
self.document = val;
self
}
/// Thumbnail of the file sent; can be ignored if thumbnail generation for
/// the file is supported server-side.
///
/// The thumbnail should be in JPEG format and less than 200 kB in size. A
/// thumbnails width and height should not exceed 320. Ignored if the
/// file is not uploaded using `multipart/form-data`. Thumbnails cant
/// be reused and can be only uploaded as a new file, so you can pass
/// “attach://<file_attach_name>” if the thumbnail was uploaded using
/// `multipart/form-data` under `<file_attach_name>`. [More info on
/// Sending Files »].
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn thumb(mut self, val: InputFile) -> Self {
self.thumb = Some(val);
self
}
/// Document caption (may also be used when resending documents by
/// `file_id`), 0-1024 characters.
pub fn caption<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.caption = Some(val.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,103 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{InlineKeyboardMarkup, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to send a game.
///
/// [The official docs](https://core.telegram.org/bots/api#sendgame).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendGame {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: i32,
game_short_name: String,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for SendGame {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendGame",
&self,
)
.await
}
}
impl SendGame {
pub(crate) fn new<G>(
bot: Arc<Bot>,
chat_id: i32,
game_short_name: G,
) -> Self
where
G: Into<String>,
{
let game_short_name = game_short_name.into();
Self {
bot,
chat_id,
game_short_name,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat.
pub fn chat_id(mut self, val: i32) -> Self {
self.chat_id = val;
self
}
/// Short name of the game, serves as the unique identifier for the game.
/// Set up your games via [@Botfather].
///
/// [@Botfather]: https://t.me/botfather
pub fn game_short_name<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.game_short_name = val.into();
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// A JSON-serialized object for an [inline keyboard]. If empty, one `Play
/// game_title` button will be shown. If not empty, the first button must
/// launch the game.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,306 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{InlineKeyboardMarkup, LabeledPrice, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to send invoices.
///
/// [The official docs](https://core.telegram.org/bots/api#sendinvoice).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendInvoice {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: i32,
title: String,
description: String,
payload: String,
provider_token: String,
start_parameter: String,
currency: String,
prices: Vec<LabeledPrice>,
provider_data: Option<String>,
photo_url: Option<String>,
photo_size: Option<i32>,
photo_width: Option<i32>,
photo_height: Option<i32>,
need_name: Option<bool>,
need_phone_number: Option<bool>,
need_email: Option<bool>,
need_shipping_address: Option<bool>,
send_phone_number_to_provider: Option<bool>,
send_email_to_provider: Option<bool>,
is_flexible: Option<bool>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<InlineKeyboardMarkup>,
}
#[async_trait::async_trait]
impl Request for SendInvoice {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendInvoice",
&self,
)
.await
}
}
impl SendInvoice {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new<T, D, Pl, Pt, S, C, Pr>(
bot: Arc<Bot>,
chat_id: i32,
title: T,
description: D,
payload: Pl,
provider_token: Pt,
start_parameter: S,
currency: C,
prices: Pr,
) -> Self
where
T: Into<String>,
D: Into<String>,
Pl: Into<String>,
Pt: Into<String>,
S: Into<String>,
C: Into<String>,
Pr: Into<Vec<LabeledPrice>>,
{
let title = title.into();
let description = description.into();
let payload = payload.into();
let provider_token = provider_token.into();
let start_parameter = start_parameter.into();
let currency = currency.into();
let prices = prices.into();
Self {
bot,
chat_id,
title,
description,
payload,
provider_token,
start_parameter,
currency,
prices,
provider_data: None,
photo_url: None,
photo_size: None,
photo_width: None,
photo_height: None,
need_name: None,
need_phone_number: None,
need_email: None,
need_shipping_address: None,
send_phone_number_to_provider: None,
send_email_to_provider: None,
is_flexible: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target private chat.
pub fn chat_id(mut self, val: i32) -> Self {
self.chat_id = val;
self
}
/// Product name, 1-32 characters.
pub fn title<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.title = val.into();
self
}
/// Product description, 1-255 characters.
pub fn description<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.description = val.into();
self
}
/// Bot-defined invoice payload, 1-128 bytes. This will not be displayed to
/// the user, use for your internal processes.
pub fn payload<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.payload = val.into();
self
}
/// Payments provider token, obtained via [@Botfather].
///
/// [@Botfather]: https://t.me/botfather
pub fn provider_token<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.provider_token = val.into();
self
}
/// Unique deep-linking parameter that can be used to generate this invoice
/// when used as a start parameter.
pub fn start_parameter<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.start_parameter = val.into();
self
}
/// Three-letter ISO 4217 currency code, see [more on currencies].
///
/// [more on currencies]: https://core.telegram.org/bots/payments#supported-currencies
pub fn currency<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.currency = val.into();
self
}
/// Price breakdown, a list of components (e.g. product price, tax,
/// discount, delivery cost, delivery tax, bonus, etc.).
pub fn prices<T>(mut self, val: T) -> Self
where
T: Into<Vec<LabeledPrice>>,
{
self.prices = val.into();
self
}
/// JSON-encoded data about the invoice, which will be shared with the
/// payment provider.
///
/// A detailed description of required fields should be provided by the
/// payment provider.
pub fn provider_data<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.provider_data = Some(val.into());
self
}
/// URL of the product photo for the invoice.
///
/// Can be a photo of the goods or a marketing image for a service. People
/// like it better when they see what they are paying for.
pub fn photo_url<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.photo_url = Some(val.into());
self
}
/// Photo size.
pub fn photo_size(mut self, val: i32) -> Self {
self.photo_size = Some(val);
self
}
/// Photo width.
pub fn photo_width(mut self, val: i32) -> Self {
self.photo_width = Some(val);
self
}
/// Photo height.
pub fn photo_height(mut self, val: i32) -> Self {
self.photo_height = Some(val);
self
}
/// Pass `true`, if you require the user's full name to complete the order.
pub fn need_name(mut self, val: bool) -> Self {
self.need_name = Some(val);
self
}
/// Pass `true`, if you require the user's phone number to complete the
/// order.
pub fn need_phone_number(mut self, val: bool) -> Self {
self.need_phone_number = Some(val);
self
}
/// Pass `true`, if you require the user's email address to complete the
/// order.
pub fn need_email(mut self, val: bool) -> Self {
self.need_email = Some(val);
self
}
/// Pass `true`, if you require the user's shipping address to complete the
/// order.
pub fn need_shipping_address(mut self, val: bool) -> Self {
self.need_shipping_address = Some(val);
self
}
/// Pass `true`, if user's phone number should be sent to provider.
pub fn send_phone_number_to_provider(mut self, val: bool) -> Self {
self.send_phone_number_to_provider = Some(val);
self
}
/// Pass `true`, if user's email address should be sent to provider.
pub fn send_email_to_provider(mut self, val: bool) -> Self {
self.send_email_to_provider = Some(val);
self
}
/// Pass `true`, if the final price depends on the shipping method.
#[allow(clippy::wrong_self_convention)]
pub fn is_flexible(mut self, val: bool) -> Self {
self.is_flexible = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// A JSON-serialized object for an [inline keyboard].
///
/// If empty, one 'Pay `total price`' button will be shown. If not empty,
/// the first button must be a Pay button.
///
/// [inlint keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: InlineKeyboardMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,122 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send point on the map.
///
/// [The official docs](https://core.telegram.org/bots/api#sendlocation).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendLocation {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
latitude: f32,
longitude: f32,
live_period: Option<i64>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendLocation {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendLocation",
&self,
)
.await
}
}
impl SendLocation {
pub(crate) fn new<C>(
bot: Arc<Bot>,
chat_id: C,
latitude: f32,
longitude: f32,
) -> Self
where
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
latitude,
longitude,
live_period: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Latitude of the location.
pub fn latitude(mut self, val: f32) -> Self {
self.latitude = val;
self
}
/// Longitude of the location.
pub fn longitude(mut self, val: f32) -> Self {
self.longitude = val;
self
}
/// Period in seconds for which the location will be updated (see [Live
/// Locations], should be between 60 and 86400).
///
/// [Live Locations]: https://telegram.org/blog/live-locations
pub fn live_period(mut self, val: i64) -> Self {
self.live_period = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// A JSON-serialized object for an [inline keyboard].
///
/// If empty, one 'Pay `total price`' button will be shown. If not empty,
/// the first button must be a Pay button.
///
/// [inlint keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,96 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputMedia, Message},
Bot,
};
use std::sync::Arc;
/// Use this method to send a group of photos or videos as an album.
///
/// [The official docs](https://core.telegram.org/bots/api#sendmediagroup).
#[derive(Debug, Clone)]
pub struct SendMediaGroup {
bot: Arc<Bot>,
chat_id: ChatId,
media: Vec<InputMedia>, // TODO: InputMediaPhoto and InputMediaVideo
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
}
#[async_trait::async_trait]
impl Request for SendMediaGroup {
type Output = Vec<Message>;
async fn send(&self) -> ResponseResult<Vec<Message>> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendMediaGroup",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("media", &self.media)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.build(),
)
.await
}
}
impl SendMediaGroup {
pub(crate) fn new<C, M>(bot: Arc<Bot>, chat_id: C, media: M) -> Self
where
C: Into<ChatId>,
M: Into<Vec<InputMedia>>,
{
let chat_id = chat_id.into();
let media = media.into();
Self {
bot,
chat_id,
media,
disable_notification: None,
reply_to_message_id: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// A JSON-serialized array describing photos and videos to be sent, must
/// include 210 items.
pub fn media<T>(mut self, val: T) -> Self
where
T: Into<Vec<InputMedia>>,
{
self.media = val.into();
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the messages are a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
}

View file

@ -0,0 +1,128 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send text messages.
///
/// [The official docs](https://core.telegram.org/bots/api#sendmessage).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendMessage {
#[serde(skip_serializing)]
bot: Arc<Bot>,
pub chat_id: ChatId,
pub text: String,
pub parse_mode: Option<ParseMode>,
pub disable_web_page_preview: Option<bool>,
pub disable_notification: Option<bool>,
pub reply_to_message_id: Option<i32>,
pub reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendMessage {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendMessage",
&self,
)
.await
}
}
impl SendMessage {
pub(crate) fn new<C, T>(bot: Arc<Bot>, chat_id: C, text: T) -> Self
where
C: Into<ChatId>,
T: Into<String>,
{
Self {
bot,
chat_id: chat_id.into(),
text: text.into(),
parse_mode: None,
disable_web_page_preview: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, value: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = value.into();
self
}
/// Text of the message to be sent.
pub fn text<T>(mut self, value: T) -> Self
where
T: Into<String>,
{
self.text = value.into();
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, value: ParseMode) -> Self {
self.parse_mode = Some(value);
self
}
/// Disables link previews for links in this message.
pub fn disable_web_page_preview(mut self, value: bool) -> Self {
self.disable_web_page_preview = Some(value);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, value: bool) -> Self {
self.disable_notification = Some(value);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, value: i32) -> Self {
self.reply_to_message_id = Some(value);
self
}
/// Additional interface options.
///
/// A JSON-serialized object for an [inline keyboard], [custom reply
/// keyboard], instructions to remove reply keyboard or to force a reply
/// from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup<T>(mut self, value: T) -> Self
where
T: Into<ReplyMarkup>,
{
self.reply_markup = Some(value.into());
self
}
}

View file

@ -0,0 +1,145 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send photos.
///
/// [The official docs](https://core.telegram.org/bots/api#sendphoto).
#[derive(Debug, Clone)]
pub struct SendPhoto {
bot: Arc<Bot>,
chat_id: ChatId,
photo: InputFile,
caption: Option<String>,
parse_mode: Option<ParseMode>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendPhoto {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendPhoto",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("photo", &self.photo)
.await
.add("caption", &self.caption)
.await
.add("parse_mode", &self.parse_mode)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendPhoto {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, photo: InputFile) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
photo,
caption: None,
parse_mode: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Photo to send.
///
/// Pass [`InputFile::File`] to send a photo that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn photo(mut self, val: InputFile) -> Self {
self.photo = val;
self
}
///Photo caption (may also be used when resending photos by file_id),
/// 0-1024 characters.
pub fn caption<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.caption = Some(val.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options. A JSON-serialized object for an [inline
/// keyboard], [custom reply keyboard], instructions to remove reply
/// keyboard or to force a reply from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,186 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message, PollType, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send a native poll.
///
/// [The official docs](https://core.telegram.org/bots/api#sendpoll).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendPoll {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
question: String,
options: Vec<String>,
is_anonymous: Option<bool>,
poll_type: Option<PollType>,
allows_multiple_answers: Option<bool>,
correct_option_id: Option<i32>,
is_closed: Option<bool>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendPoll {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendPoll",
&self,
)
.await
}
}
impl SendPoll {
pub(crate) fn new<C, Q, O>(
bot: Arc<Bot>,
chat_id: C,
question: Q,
options: O,
) -> Self
where
C: Into<ChatId>,
Q: Into<String>,
O: Into<Vec<String>>,
{
let chat_id = chat_id.into();
let question = question.into();
let options = options.into();
Self {
bot,
chat_id,
question,
options,
is_anonymous: None,
poll_type: None,
allows_multiple_answers: None,
correct_option_id: None,
is_closed: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
///
/// A native poll can't be sent to a private chat.
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Poll question, 1-255 characters.
pub fn question<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.question = val.into();
self
}
/// List of answer options, 2-10 strings 1-100 characters each.
pub fn options<T>(mut self, val: T) -> Self
where
T: Into<Vec<String>>,
{
self.options = val.into();
self
}
/// `true`, if the poll needs to be anonymous, defaults to `true`.
#[allow(clippy::wrong_self_convention)]
pub fn is_anonymous<T>(mut self, val: T) -> Self
where
T: Into<bool>,
{
self.is_anonymous = Some(val.into());
self
}
/// Poll type, `quiz` or `regular`, defaults to `regular`.
pub fn poll_type(mut self, val: PollType) -> Self {
self.poll_type = Some(val);
self
}
/// `true`, if the poll allows multiple answers, ignored for polls in quiz
/// mode.
///
/// Defaults to `false`.
pub fn allows_multiple_answers<T>(mut self, val: T) -> Self
where
T: Into<bool>,
{
self.allows_multiple_answers = Some(val.into());
self
}
/// 0-based identifier of the correct answer option, required for polls in
/// quiz mode.
pub fn correct_option_id<T>(mut self, val: T) -> Self
where
T: Into<i32>,
{
self.correct_option_id = Some(val.into());
self
}
/// Pass `true`, if the poll needs to be immediately closed.
///
/// This can be useful for poll preview.
#[allow(clippy::wrong_self_convention)]
pub fn is_closed<T>(mut self, val: T) -> Self
where
T: Into<bool>,
{
self.is_closed = Some(val.into());
self
}
/// Sends the message [silently].
///
/// Users will receive a notification with no sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
///
/// A JSON-serialized object for an [inline keyboard], [custom reply
/// keyboard], instructions to remove reply keyboard or to force a reply
/// from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,118 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send static .WEBP or [animated] .TGS stickers.
///
/// [The official docs](https://core.telegram.org/bots/api#sendsticker).
///
/// [animated]: https://telegram.org/blog/animated-stickers
#[derive(Debug, Clone)]
pub struct SendSticker {
bot: Arc<Bot>,
chat_id: ChatId,
sticker: InputFile,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendSticker {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendSticker",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("sticker", &self.sticker)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendSticker {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, sticker: InputFile) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
sticker,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Sticker to send.
///
/// Pass [`InputFile::File`] to send a file that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn sticker(mut self, val: InputFile) -> Self {
self.sticker = val;
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
///
/// A JSON-serialized object for an [inline keyboard], [custom reply
/// keyboard], instructions to remove reply keyboard or to force a reply
/// from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,166 @@
use serde::Serialize;
use crate::{
net,
requests::{Request, ResponseResult},
types::{ChatId, Message, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send information about a venue.
///
/// [The official docs](https://core.telegram.org/bots/api#sendvenue).
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendVenue {
#[serde(skip_serializing)]
bot: Arc<Bot>,
chat_id: ChatId,
latitude: f32,
longitude: f32,
title: String,
address: String,
foursquare_id: Option<String>,
foursquare_type: Option<String>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendVenue {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_json(
self.bot.client(),
self.bot.token(),
"sendVenue",
&self,
)
.await
}
}
impl SendVenue {
pub(crate) fn new<C, T, A>(
bot: Arc<Bot>,
chat_id: C,
latitude: f32,
longitude: f32,
title: T,
address: A,
) -> Self
where
C: Into<ChatId>,
T: Into<String>,
A: Into<String>,
{
let chat_id = chat_id.into();
let title = title.into();
let address = address.into();
Self {
bot,
chat_id,
latitude,
longitude,
title,
address,
foursquare_id: None,
foursquare_type: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Latitude of the venue.
pub fn latitude(mut self, val: f32) -> Self {
self.latitude = val;
self
}
/// Longitude of the venue.
pub fn longitude(mut self, val: f32) -> Self {
self.longitude = val;
self
}
/// Name of the venue.
pub fn title<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.title = val.into();
self
}
/// Address of the venue.
pub fn address<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.address = val.into();
self
}
/// Foursquare identifier of the venue.
pub fn foursquare_id<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.foursquare_id = Some(val.into());
self
}
/// Foursquare type of the venue, if known.
///
/// For example, `arts_entertainment/default`, `arts_entertainment/aquarium`
/// or `food/icecream`.
pub fn foursquare_type<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.foursquare_type = Some(val.into());
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
///
/// A JSON-serialized object for an [inline keyboard], [custom reply
/// keyboard], instructions to remove reply keyboard or to force a reply
/// from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

View file

@ -0,0 +1,210 @@
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
use std::sync::Arc;
/// Use this method to send video files, Telegram clients support mp4 videos
/// (other formats may be sent as Document).
///
/// Bots can currently send video files of up to 50 MB in size, this
/// limit may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#sendvideo).
#[derive(Debug, Clone)]
pub struct SendVideo {
bot: Arc<Bot>,
chat_id: ChatId,
video: InputFile,
duration: Option<i32>,
width: Option<i32>,
height: Option<i32>,
thumb: Option<InputFile>,
caption: Option<String>,
parse_mode: Option<ParseMode>,
supports_streaming: Option<bool>,
disable_notification: Option<bool>,
reply_to_message_id: Option<i32>,
reply_markup: Option<ReplyMarkup>,
}
#[async_trait::async_trait]
impl Request for SendVideo {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendVideo",
FormBuilder::new()
.add("chat_id", &self.chat_id)
.await
.add("video", &self.video)
.await
.add("duration", &self.duration)
.await
.add("width", &self.width)
.await
.add("height", &self.height)
.await
.add("thumb", &self.thumb)
.await
.add("caption", &self.caption)
.await
.add("parse_mode", &self.parse_mode)
.await
.add("supports_streaming", &self.supports_streaming)
.await
.add("disable_notification", &self.disable_notification)
.await
.add("reply_to_message_id", &self.reply_to_message_id)
.await
.add("reply_markup", &self.reply_markup)
.await
.build(),
)
.await
}
}
impl SendVideo {
pub(crate) fn new<C>(bot: Arc<Bot>, chat_id: C, video: InputFile) -> Self
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
video,
duration: None,
width: None,
height: None,
thumb: None,
caption: None,
parse_mode: None,
supports_streaming: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None,
}
}
/// Unique identifier for the target chat or username of the target channel
/// (in the format `@channelusername`).
pub fn chat_id<T>(mut self, val: T) -> Self
where
T: Into<ChatId>,
{
self.chat_id = val.into();
self
}
/// Video to sent.
///
/// Pass [`InputFile::File`] to send a file that exists on
/// the Telegram servers (recommended), pass an [`InputFile::Url`] for
/// Telegram to get a .webp file from the Internet, or upload a new one
/// using [`InputFile::FileId`]. [More info on Sending Files »].
///
/// [`InputFile::File`]: crate::types::InputFile::File
/// [`InputFile::Url`]: crate::types::InputFile::Url
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
pub fn video(mut self, val: InputFile) -> Self {
self.video = val;
self
}
/// Duration of sent video in seconds.
pub fn duration(mut self, val: i32) -> Self {
self.duration = Some(val);
self
}
/// Video width.
pub fn width(mut self, val: i32) -> Self {
self.width = Some(val);
self
}
/// Video height.
pub fn height(mut self, val: i32) -> Self {
self.height = Some(val);
self
}
/// Thumbnail of the file sent; can be ignored if thumbnail generation for
/// the file is supported server-side.
///
/// The thumbnail should be in JPEG format and less than 200 kB in size. A
/// thumbnails width and height should not exceed 320. Ignored if the
/// file is not uploaded using `multipart/form-data`. Thumbnails cant be
/// reused and can be only uploaded as a new file, so you can pass
/// `attach://<file_attach_name>` if the thumbnail was uploaded using
/// `multipart/form-data` under `<file_attach_name>`. [More info on Sending
/// Files »].
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
pub fn thumb(mut self, val: InputFile) -> Self {
self.thumb = Some(val);
self
}
/// Video caption (may also be used when resending videos by file_id),
/// 0-1024 characters.
pub fn caption<T>(mut self, val: T) -> Self
where
T: Into<String>,
{
self.caption = Some(val.into());
self
}
/// Send [Markdown] or [HTML], if you want Telegram apps to show
/// [bold, italic, fixed-width text or inline URLs] in the media caption.
///
/// [Markdown]: crate::types::ParseMode::Markdown
/// [HTML]: crate::types::ParseMode::HTML
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::types::ParseMode
pub fn parse_mode(mut self, val: ParseMode) -> Self {
self.parse_mode = Some(val);
self
}
/// Pass `true`, if the uploaded video is suitable for streaming.
pub fn supports_streaming(mut self, val: bool) -> Self {
self.supports_streaming = Some(val);
self
}
/// Sends the message [silently]. Users will receive a notification with no
/// sound.
///
/// [silently]: https://telegram.org/blog/channels-2-0#silent-messages
pub fn disable_notification(mut self, val: bool) -> Self {
self.disable_notification = Some(val);
self
}
/// If the message is a reply, ID of the original message.
pub fn reply_to_message_id(mut self, val: i32) -> Self {
self.reply_to_message_id = Some(val);
self
}
/// Additional interface options.
///
/// A JSON-serialized object for an [inline keyboard], [custom reply
/// keyboard], instructions to remove reply keyboard or to force a reply
/// from the user.
///
/// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating
/// [custom reply keyboard]: https://core.telegram.org/bots#keyboards
pub fn reply_markup(mut self, val: ReplyMarkup) -> Self {
self.reply_markup = Some(val);
self
}
}

Some files were not shown because too many files have changed in this diff Show more