use std::ffi::{CStr, CString, c_char};
use std::future::Future;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use serde_json::json;
use tdlib_rs::{enums, functions, types};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::mpsc::error::SendError;
use crate::chats::{Badge, Peer};
use crate::config::{ApiKeys, Config};
use crate::reactions::{self, Available, ReactionKind};
use crate::stickers::{Source, Sticker};
pub enum TgEvent {
Update(Box<enums::Update>),
Error(String),
ChatsLoaded {
all: bool,
failed: bool,
},
History {
chat_id: i64,
page: Page,
messages: Option<Vec<types::Message>>,
},
Found {
chat_id: i64,
query: String,
found: Option<Found>,
},
Replied {
chat_id: i64,
message_id: i64,
replied: Option<Box<types::Message>>,
},
Deletable {
chat_id: i64,
message_id: i64,
deletable: Option<Deletable>,
},
Editable {
chat_id: i64,
message_id: i64,
editable: Option<bool>,
text: Option<EditText>,
},
Reactions {
chat_id: i64,
message_id: i64,
available: Option<Available>,
},
Stickers {
source: Source,
stickers: Option<Vec<types::Sticker>>,
},
StickerSets(Option<Vec<(i64, String)>>),
StickersFound {
query: String,
stickers: Option<Vec<types::Sticker>>,
},
Downloaded {
file_id: i32,
path: Option<String>,
},
ChatsFound {
query: String,
chat_ids: Vec<i64>,
user_ids: Vec<i64>,
},
ChatFound {
request: String,
found: Result<(i64, Option<i64>), Missed>,
},
Members {
chat_id: i64,
query: String,
user_ids: Vec<i64>,
},
Commands {
chat_id: i64,
commands: Vec<crate::complete::Command>,
},
Forwarded {
chat_id: i64,
},
Joined {
chat_id: i64,
},
Left {
chat_id: i64,
},
Invite {
request: String,
link: String,
invite: Invite,
},
}
#[derive(Debug, PartialEq, Eq)]
pub enum Missed {
Said(String),
Quiet,
Elsewhere,
}
impl From<String> for Missed {
fn from(why: String) -> Self {
Missed::Said(why)
}
}
pub struct Invite {
pub title: String,
pub members: i32,
pub channel: bool,
pub badge: Option<Badge>,
pub by_request: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Page {
Latest,
Older(i64),
Newer(i64),
Around(i64),
}
pub struct Found {
pub ids: Vec<i64>,
pub total: i32,
pub next_from: i64,
}
#[derive(Clone, Copy)]
pub struct Deletable {
pub for_everyone: bool,
pub for_me: bool,
}
const DOWNLOAD_PRIORITY: i32 = 16;
const QUIET_DOWNLOAD_PRIORITY: i32 = 8;
const NOTIFICATION_GROUPS: i64 = 5;
const STICKER_SEARCH_LIMIT: i32 = 100;
const MEMBER_SEARCH_LIMIT: i32 = 20;
const CONTACT_SEARCH_LIMIT: i32 = 20;
const OFFLINE_GRACE: Duration = Duration::from_millis(300);
static CLIENT: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" {
fn td_execute(request: *const c_char) -> *const c_char;
fn td_send(client_id: i32, request: *const c_char);
fn OPENSSL_init_crypto(opts: u64, settings: *const std::ffi::c_void) -> std::ffi::c_int;
}
const OPENSSL_INIT_NO_LOAD_CONFIG: u64 = 0x80;
fn no_openssl_config() -> Result<()> {
let done = unsafe { OPENSSL_init_crypto(OPENSSL_INIT_NO_LOAD_CONFIG, std::ptr::null()) };
anyhow::ensure!(done == 1, "OpenSSL couldn't be set up");
Ok(())
}
pub fn offline_now() {
let client_id = CLIENT.load(Ordering::Relaxed);
if client_id == 0 {
return;
}
let request = json!({
"@type": "setOption",
"name": "online",
"value": { "@type": "optionValueBoolean", "value": false },
});
let Ok(request) = CString::new(request.to_string()) else {
return;
};
unsafe { td_send(client_id, request.as_ptr()) };
std::thread::sleep(OFFLINE_GRACE);
}
fn log_to_file(path: &Path) -> Result<()> {
let requests = [
json!({
"@type": "setLogStream",
"log_stream": {
"@type": "logStreamFile",
"path": path.to_string_lossy(),
"max_file_size": 10 * 1024 * 1024,
"redirect_stderr": false,
},
}),
json!({ "@type": "setLogVerbosityLevel", "new_verbosity_level": 2 }),
];
for request in requests {
let response = execute(&request)?;
if !response.contains(r#""@type":"ok""#) {
bail!("TDLib log setup failed: {response}");
}
}
Ok(())
}
fn execute(request: &serde_json::Value) -> Result<String> {
let request = CString::new(request.to_string())?;
let response = unsafe {
let response = td_execute(request.as_ptr());
if response.is_null() {
String::new()
} else {
CStr::from_ptr(response).to_string_lossy().into_owned()
}
};
Ok(response)
}
fn execute_text(request: &serde_json::Value) -> Option<types::FormattedText> {
let response = execute(request).ok()?;
match serde_json::from_str(&response) {
Ok(enums::FormattedText::FormattedText(text)) => Some(text),
Err(_) => None,
}
}
pub fn markdown(text: String) -> types::FormattedText {
let request = json!({
"@type": "parseMarkdown",
"text": { "@type": "formattedText", "text": text, "entities": [] },
});
let mut parsed = execute_text(&request).unwrap_or_else(|| plain(text));
for entity in &mut parsed.entities {
if let enums::TextEntityType::TextUrl(link) = &entity.r#type
&& let Some(user_id) = link
.url
.strip_prefix("tg://user?id=")
.and_then(|id| id.parse().ok())
{
entity.r#type =
enums::TextEntityType::MentionName(types::TextEntityTypeMentionName { user_id });
}
}
parsed
}
pub struct EditText {
pub markdown: String,
pub loses: bool,
}
pub fn to_markdown(text: &types::FormattedText) -> Option<EditText> {
let request = json!({ "@type": "getMarkdownText", "text": text });
let written = execute_text(&request)?;
let mut markdown = written.text.clone();
let mut loses = false;
let mut left = written.entities.clone();
left.sort_by_key(|e| std::cmp::Reverse(e.offset));
for entity in left {
match &entity.r#type {
enums::TextEntityType::MentionName(mention) => {
let start = crate::messages::byte_offset(&written.text, entity.offset);
let end = crate::messages::byte_offset(
&written.text,
entity.offset.saturating_add(entity.length),
);
if start < end {
markdown.insert_str(end, &format!("](tg://user?id={})", mention.user_id));
markdown.insert(start, '[');
}
}
kind if !crate::messages::found_by_telegram(kind) => loses = true,
_ => {}
}
}
Some(EditText {
markdown: crate::text::clean(&markdown),
loses,
})
}
fn editable_text(content: &enums::MessageContent) -> Option<&types::FormattedText> {
use enums::MessageContent as C;
match content {
C::MessageText(m) => Some(&m.text),
C::MessagePhoto(m) => Some(&m.caption),
C::MessageVideo(m) => Some(&m.caption),
C::MessageAnimation(m) => Some(&m.caption),
C::MessageDocument(m) => Some(&m.caption),
C::MessageAudio(m) => Some(&m.caption),
C::MessageVoiceNote(m) => Some(&m.caption),
_ => None,
}
}
pub type Tagged = (i32, TgEvent);
#[derive(Clone)]
struct Events {
client_id: i32,
tx: UnboundedSender<Tagged>,
}
impl Events {
fn send(&self, event: TgEvent) -> Result<(), SendError<Tagged>> {
self.tx.send((self.client_id, event))
}
}
#[derive(Clone)]
pub struct Tg {
client_id: i32,
tx: Events,
config: Arc<Config>,
}
impl Tg {
pub async fn start(config: Config, tx: UnboundedSender<Tagged>) -> Result<Self> {
no_openssl_config()?;
log_to_file(&config.data_dir.join("tdlib.log"))?;
let client_id = tdlib_rs::create_client();
CLIENT.store(client_id, Ordering::Relaxed);
std::thread::spawn({
let tx = tx.clone();
move || {
loop {
if let Some((update, client_id)) = tdlib_rs::receive()
&& tx
.send((client_id, TgEvent::Update(Box::new(update))))
.is_err()
{
break;
}
}
}
});
functions::get_option("version".into(), client_id)
.await
.map_err(|e| anyhow!("TDLib didn't start: {}", e.message))?;
Ok(Self {
client_id,
tx: Events { client_id, tx },
config: Arc::new(config),
})
}
pub fn detached(tx: UnboundedSender<Tagged>) -> Self {
let client_id = 0;
Self {
client_id,
tx: Events { client_id, tx },
config: Arc::new(Config {
api_keys: None,
data_dir: std::env::temp_dir(),
}),
}
}
pub fn client_id(&self) -> i32 {
self.client_id
}
pub fn set_tdlib_parameters(&self, keys: ApiKeys) {
let config = Arc::clone(&self.config);
let client_id = self.client_id;
self.spawn(async move {
let dir = |name: &str| config.data_dir.join(name).to_string_lossy().into_owned();
functions::set_tdlib_parameters(
false,
dir("db"),
dir("files"),
String::new(), true, true, true, false, keys.id,
keys.hash,
"en".into(),
"Terminal".into(), std::env::consts::OS.into(),
env!("CARGO_PKG_VERSION").into(),
client_id,
)
.await
});
}
pub fn send_phone_number(&self, phone: String) {
self.spawn(functions::set_authentication_phone_number(
phone,
None,
self.client_id,
));
}
pub fn send_code(&self, code: String) {
self.spawn(functions::check_authentication_code(code, self.client_id));
}
pub fn send_password(&self, password: String) {
self.spawn(functions::check_authentication_password(
password,
self.client_id,
));
}
pub fn send_email(&self, email: String) {
self.spawn(functions::set_authentication_email_address(
email,
self.client_id,
));
}
pub fn request_qr_code(&self) {
self.spawn(functions::request_qr_code_authentication(
Vec::new(),
self.client_id,
));
}
pub fn send_email_code(&self, code: String) {
self.spawn(functions::check_authentication_email_code(
enums::EmailAddressAuthentication::Code(types::EmailAddressAuthenticationCode { code }),
self.client_id,
));
}
pub fn load_chats(&self, limit: i32) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let (all, failed) =
match functions::load_chats(Some(enums::ChatList::Main), limit, client_id).await {
Ok(()) => (false, false),
Err(e) if e.code == 404 => (true, false),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
(false, true)
}
};
let _ = tx.send(TgEvent::ChatsLoaded { all, failed });
});
}
pub fn open_chat(&self, chat_id: i64) {
self.spawn(functions::open_chat(chat_id, self.client_id));
}
pub fn close_chat(&self, chat_id: i64) {
self.spawn(functions::close_chat(chat_id, self.client_id));
}
pub fn load_history(&self, chat_id: i64, page: Page, limit: i32) {
let (from, offset) = match page {
Page::Latest => (0, 0),
Page::Older(id) => (id, 0),
Page::Newer(id) => (id, 1 - limit),
Page::Around(id) => (id, -limit / 2),
};
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result =
functions::get_chat_history(chat_id, from, offset, limit, false, client_id).await;
let messages = match result {
Ok(enums::Messages::Messages(page)) => {
Some(page.messages.into_iter().flatten().collect())
}
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::History {
chat_id,
page,
messages,
});
});
}
pub fn search_messages(&self, chat_id: i64, query: String, from: i64, limit: i32) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::search_chat_messages(
chat_id,
None,
query.clone(),
None,
from,
0,
limit,
None,
client_id,
)
.await;
let found = match result {
Ok(enums::FoundChatMessages::FoundChatMessages(f)) => Some(Found {
ids: f.messages.iter().map(|m| m.id).collect(),
total: f.total_count,
next_from: f.next_from_message_id,
}),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::Found {
chat_id,
query,
found,
});
});
}
pub fn get_replied_message(&self, chat_id: i64, message_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let replied = functions::get_replied_message(chat_id, message_id, client_id)
.await
.ok()
.map(|enums::Message::Message(m)| Box::new(m));
let _ = tx.send(TgEvent::Replied {
chat_id,
message_id,
replied,
});
});
}
pub fn check_deletable(&self, chat_id: i64, message_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::get_message_properties(chat_id, message_id, client_id).await;
let deletable = match result {
Ok(enums::MessageProperties::MessageProperties(p)) => Some(Deletable {
for_everyone: p.can_be_deleted_for_all_users,
for_me: p.can_be_deleted_only_for_self,
}),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::Deletable {
chat_id,
message_id,
deletable,
});
});
}
pub fn available_reactions(&self, chat_id: i64, message_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let row = reactions::COLUMNS as i32;
let result =
functions::get_message_available_reactions(chat_id, message_id, row, client_id)
.await;
let available = match result {
Ok(enums::AvailableReactions::AvailableReactions(r)) => Some(Available::from(r)),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::Reactions {
chat_id,
message_id,
available,
});
});
}
pub fn react(&self, chat_id: i64, message_id: i64, kind: &ReactionKind, add: bool) {
let Some(reaction) = kind.to_tdlib() else {
return;
};
if add {
self.spawn(functions::add_message_reaction(
chat_id,
message_id,
reaction,
false,
true,
self.client_id,
));
} else {
self.spawn(functions::remove_message_reaction(
chat_id,
message_id,
reaction,
self.client_id,
));
}
}
pub fn check_editable(&self, chat_id: i64, message_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::get_message_properties(chat_id, message_id, client_id).await;
let editable = match result {
Ok(enums::MessageProperties::MessageProperties(p)) => Some(p.can_be_edited),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let mut text = None;
if editable == Some(true)
&& let Ok(enums::Message::Message(message)) =
functions::get_message(chat_id, message_id, client_id).await
{
text = editable_text(&message.content).and_then(to_markdown);
}
let _ = tx.send(TgEvent::Editable {
chat_id,
message_id,
editable,
text,
});
});
}
pub fn edit_text(&self, chat_id: i64, message_id: i64, text: String) {
let content = enums::InputMessageContent::InputMessageText(types::InputMessageText {
text: markdown(text),
link_preview_options: None,
clear_draft: false,
});
self.spawn(functions::edit_message_text(
chat_id,
message_id,
content,
self.client_id,
));
}
pub fn edit_caption(&self, chat_id: i64, message_id: i64, text: String, above: bool) {
self.spawn(functions::edit_message_caption(
chat_id,
message_id,
Some(markdown(text)),
above,
self.client_id,
));
}
pub fn delete_message(&self, chat_id: i64, message_id: i64, revoke: bool) {
self.spawn(functions::delete_messages(
chat_id,
vec![message_id],
revoke,
self.client_id,
));
}
pub fn send_text(&self, chat_id: i64, text: String, reply_to: Option<i64>) {
let content = enums::InputMessageContent::InputMessageText(types::InputMessageText {
text: markdown(text),
link_preview_options: None,
clear_draft: true,
});
let reply_to = reply_to.map(reply_to_message);
self.spawn(functions::send_message(
chat_id,
None,
reply_to,
None,
content,
self.client_id,
));
}
pub fn send_sticker(&self, chat_id: i64, sticker: &Sticker, reply_to: Option<i64>) {
let content = enums::InputMessageContent::InputMessageSticker(types::InputMessageSticker {
sticker: enums::InputFile::Id(types::InputFileId {
id: sticker.file_id,
}),
thumbnail: None,
width: sticker.width,
height: sticker.height,
emoji: sticker.emoji.clone(),
});
let reply_to = reply_to.map(reply_to_message);
self.spawn(functions::send_message(
chat_id,
None,
reply_to,
None,
content,
self.client_id,
));
}
pub fn stickers(&self, source: Source) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = match source {
Source::Recent => functions::get_recent_stickers(false, client_id)
.await
.map(|enums::Stickers::Stickers(s)| s.stickers),
Source::Favorites => functions::get_favorite_stickers(client_id)
.await
.map(|enums::Stickers::Stickers(s)| s.stickers),
Source::Set(id) => functions::get_sticker_set(id, client_id)
.await
.map(|enums::StickerSet::StickerSet(s)| s.stickers),
};
let stickers = match result {
Ok(stickers) => Some(stickers),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::Stickers { source, stickers });
});
}
pub fn sticker_sets(&self) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result =
functions::get_installed_sticker_sets(enums::StickerType::Regular, client_id).await;
let sets = match result {
Ok(enums::StickerSets::StickerSets(s)) => {
Some(s.sets.into_iter().map(|set| (set.id, set.title)).collect())
}
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::StickerSets(sets));
});
}
pub fn find_stickers(&self, chat_id: i64, query: String) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::get_stickers(
enums::StickerType::Regular,
query.clone(),
STICKER_SEARCH_LIMIT,
chat_id,
client_id,
)
.await;
let stickers = match result {
Ok(enums::Stickers::Stickers(s)) => Some(s.stickers),
Err(e) => {
let _ = tx.send(TgEvent::Error(e.message));
None
}
};
let _ = tx.send(TgEvent::StickersFound { query, stickers });
});
}
pub fn send_files(
&self,
chat_id: i64,
groups: Vec<Vec<Upload>>,
caption: String,
reply_to: Option<i64>,
) {
let tx = self.tx.clone();
let client_id = self.client_id;
let mut caption = (!caption.is_empty()).then(|| markdown(caption));
tokio::spawn(async move {
let mut reply_to = reply_to.map(reply_to_message);
for group in groups {
let mut contents: Vec<_> = group
.into_iter()
.map(|upload| upload.content(caption.take()))
.collect();
let reply_to = reply_to.take();
let result = if contents.len() == 1 {
let content = contents.remove(0);
functions::send_message(chat_id, None, reply_to, None, content, client_id)
.await
.map(drop)
} else {
functions::send_message_album(
chat_id, None, reply_to, None, contents, client_id,
)
.await
.map(drop)
};
if let Err(e) = result {
let _ = tx.send(TgEvent::Error(e.message));
break;
}
}
});
}
pub fn forward(&self, chat_id: i64, from_chat_id: i64, message_ids: Vec<i64>) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::forward_messages(
chat_id,
None,
from_chat_id,
message_ids,
None,
false, false, client_id,
)
.await;
let _ = match result {
Ok(_) => tx.send(TgEvent::Forwarded { chat_id }),
Err(e) => tx.send(TgEvent::Error(e.message)),
};
});
}
pub fn find_chats(&self, query: String) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let user_ids =
match functions::search_contacts(query.clone(), CONTACT_SEARCH_LIMIT, client_id)
.await
{
Ok(enums::Users::Users(u)) => u.user_ids,
Err(_) => Vec::new(),
};
let chat_ids = match functions::search_public_chats(query.clone(), client_id).await {
Ok(enums::Chats::Chats(c)) => c.chat_ids,
Err(_) => Vec::new(),
};
let _ = tx.send(TgEvent::ChatsFound {
query,
chat_ids,
user_ids,
});
});
}
pub fn find_private_chat(&self, user_id: i64, request: String) {
let client_id = self.client_id;
self.find(request, async move {
let enums::Chat::Chat(chat) = functions::create_private_chat(user_id, false, client_id)
.await
.map_err(|e| Missed::Said(e.message))?;
Ok((chat.id, None))
});
}
pub fn find_username(&self, username: String, request: String) {
let client_id = self.client_id;
self.find(request, async move {
let id = public_chat(&username, client_id).await?;
Ok((id, None))
});
}
pub fn find_link(&self, link: String, request: String) {
let tx = self.tx.clone();
let client_id = self.client_id;
self.find(request.clone(), async move {
let kind = functions::get_internal_link_type(link.clone(), client_id)
.await
.map_err(|_| Missed::Elsewhere)?;
match kind {
enums::InternalLinkType::PublicChat(p) => {
Ok((public_chat(&p.chat_username, client_id).await?, None))
}
enums::InternalLinkType::Message(m) => {
let enums::MessageLinkInfo::MessageLinkInfo(info) =
functions::get_message_link_info(m.url, client_id)
.await
.map_err(|e| Missed::Said(e.message))?;
if info.chat_id == 0 {
return Err(Missed::Said("That message can't be found".into()));
}
Ok((info.chat_id, info.message.map(|m| m.id)))
}
enums::InternalLinkType::ChatInvite(i) => {
let enums::ChatInviteLinkInfo::ChatInviteLinkInfo(info) =
functions::check_chat_invite_link(i.invite_link.clone(), client_id)
.await
.map_err(|e| Missed::Said(e.message))?;
if info.chat_id != 0 && info.accessible_for == 0 {
return Ok((info.chat_id, None));
}
let invite = Invite {
title: crate::text::clean(&info.title),
members: info.member_count,
channel: matches!(info.r#type, enums::InviteLinkChatType::Channel),
badge: Badge::of(info.verification_status.as_ref(), false),
by_request: info.creates_join_request,
};
let _ = tx.send(TgEvent::Invite {
request,
link: i.invite_link,
invite,
});
Err(Missed::Quiet)
}
_ => Err(Missed::Elsewhere),
}
});
}
pub fn join_by_link(&self, link: String, request: String) {
let client_id = self.client_id;
self.find(request, async move {
let enums::Chat::Chat(chat) = functions::join_chat_by_invite_link(link, client_id)
.await
.map_err(|e| Missed::Said(e.message))?;
Ok((chat.id, None))
});
}
pub fn join_chat(&self, chat_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let _ = match functions::join_chat(chat_id, client_id).await {
Ok(()) => tx.send(TgEvent::Joined { chat_id }),
Err(e) => tx.send(TgEvent::Error(e.message)),
};
});
}
pub fn find_members(&self, chat_id: i64, query: String) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::search_chat_members(
chat_id,
query.clone(),
MEMBER_SEARCH_LIMIT,
None,
client_id,
)
.await;
let user_ids = match result {
Ok(enums::ChatMembers::ChatMembers(m)) => m
.members
.into_iter()
.filter_map(|m| match m.member_id {
enums::MessageSender::User(u) => Some(u.user_id),
enums::MessageSender::Chat(_) => None,
})
.collect(),
Err(_) => Vec::new(),
};
let _ = tx.send(TgEvent::Members {
chat_id,
query,
user_ids,
});
});
}
pub fn bot_commands(&self, chat_id: i64, peer: Peer) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let by_bot: Vec<(i64, Vec<types::BotCommand>)> = match peer {
Peer::User(id) => match functions::get_user_full_info(id, client_id).await {
Ok(enums::UserFullInfo::UserFullInfo(info)) => info
.bot_info
.map(|b| vec![(id, b.commands)])
.unwrap_or_default(),
Err(_) => Vec::new(),
},
Peer::BasicGroup(id) => {
match functions::get_basic_group_full_info(id, client_id).await {
Ok(enums::BasicGroupFullInfo::BasicGroupFullInfo(info)) => info
.bot_commands
.into_iter()
.map(|b| (b.bot_user_id, b.commands))
.collect(),
Err(_) => Vec::new(),
}
}
Peer::Supergroup(id) => {
match functions::get_supergroup_full_info(id, client_id).await {
Ok(enums::SupergroupFullInfo::SupergroupFullInfo(info)) => info
.bot_commands
.into_iter()
.map(|b| (b.bot_user_id, b.commands))
.collect(),
Err(_) => Vec::new(),
}
}
};
let line = |text: &str| {
crate::text::clean(text)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
};
let commands = by_bot
.into_iter()
.flat_map(|(bot, commands)| {
commands.into_iter().map(move |c| crate::complete::Command {
bot,
name: line(&c.command),
description: line(&c.description),
})
})
.collect();
let _ = tx.send(TgEvent::Commands { chat_id, commands });
});
}
pub fn pin_chat(&self, chat_id: i64, pinned: bool) {
self.spawn(functions::toggle_chat_is_pinned(
enums::ChatList::Main,
chat_id,
pinned,
self.client_id,
));
}
pub fn set_notifications(&self, chat_id: i64, settings: types::ChatNotificationSettings) {
self.spawn(functions::set_chat_notification_settings(
chat_id,
settings,
self.client_id,
));
}
pub fn vote(&self, chat_id: i64, message_id: i64, answers: Vec<i32>) {
self.spawn(functions::set_poll_answer(
chat_id,
message_id,
answers,
self.client_id,
));
}
pub fn leave_chat(&self, chat_id: i64) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let _ = match functions::leave_chat(chat_id, client_id).await {
Ok(()) => tx.send(TgEvent::Left { chat_id }),
Err(e) => tx.send(TgEvent::Error(e.message)),
};
});
}
fn find(
&self,
request: String,
lookup: impl Future<Output = Result<(i64, Option<i64>), Missed>> + Send + 'static,
) {
let tx = self.tx.clone();
tokio::spawn(async move {
let found = lookup.await;
let _ = tx.send(TgEvent::ChatFound { request, found });
});
}
pub fn send_typing(&self, chat_id: i64, typing: bool) {
let action = typing.then_some(enums::ChatAction::Typing);
let client_id = self.client_id;
tokio::spawn(async move {
let _ = functions::send_chat_action(chat_id, None, action, client_id).await;
});
}
pub fn download(&self, file_id: i32) {
self.fetch_file(file_id, DOWNLOAD_PRIORITY, true);
}
pub fn download_quiet(&self, file_id: i32) {
self.fetch_file(file_id, QUIET_DOWNLOAD_PRIORITY, false);
}
fn fetch_file(&self, file_id: i32, priority: i32, report_errors: bool) {
let tx = self.tx.clone();
let client_id = self.client_id;
tokio::spawn(async move {
let result = functions::download_file(file_id, priority, 0, 0, true, client_id).await;
let path = match result {
Ok(enums::File::File(f)) if f.local.is_downloading_completed => Some(f.local.path),
Ok(_) => None,
Err(e) => {
if report_errors {
let _ = tx.send(TgEvent::Error(e.message));
}
None
}
};
let _ = tx.send(TgEvent::Downloaded { file_id, path });
});
}
pub fn set_online(&self, online: bool) {
let value = enums::OptionValue::Boolean(types::OptionValueBoolean { value: online });
self.set_option("online", value);
}
pub fn enable_notifications(&self) {
let value = enums::OptionValue::Integer(types::OptionValueInteger {
value: NOTIFICATION_GROUPS,
});
self.set_option("notification_group_count_max", value);
}
fn set_option(&self, name: &str, value: enums::OptionValue) {
self.spawn(functions::set_option(
name.into(),
Some(value),
self.client_id,
));
}
pub fn view_messages(&self, chat_id: i64, message_ids: Vec<i64>) {
self.spawn(functions::view_messages(
chat_id,
message_ids,
None,
true,
self.client_id,
));
}
pub fn close(&self) {
let client_id = self.client_id;
self.spawn(async move {
let offline = enums::OptionValue::Boolean(types::OptionValueBoolean { value: false });
let _ = functions::set_option("online".into(), Some(offline), client_id).await;
functions::close(client_id).await
});
}
pub fn log_out(&self) {
self.spawn(functions::log_out(self.client_id));
}
pub fn reopen(&mut self) {
self.client_id = tdlib_rs::create_client();
CLIENT.store(self.client_id, Ordering::Relaxed);
self.tx.client_id = self.client_id;
self.spawn(functions::get_option("version".into(), self.client_id));
}
fn spawn<T: Send + 'static>(
&self,
request: impl Future<Output = Result<T, types::Error>> + Send + 'static,
) {
let tx = self.tx.clone();
tokio::spawn(async move {
if let Err(e) = request.await {
let _ = tx.send(TgEvent::Error(e.message));
}
});
}
}
pub enum Upload {
Photo {
path: String,
width: i32,
height: i32,
},
File { path: String },
}
impl Upload {
fn content(self, caption: Option<types::FormattedText>) -> enums::InputMessageContent {
let local = |path| enums::InputFile::Local(types::InputFileLocal { path });
match self {
Upload::Photo {
path,
width,
height,
} => enums::InputMessageContent::InputMessagePhoto(types::InputMessagePhoto {
photo: local(path),
thumbnail: None,
added_sticker_file_ids: Vec::new(),
width,
height,
caption,
show_caption_above_media: false,
self_destruct_type: None,
has_spoiler: false,
}),
Upload::File { path } => {
enums::InputMessageContent::InputMessageDocument(types::InputMessageDocument {
document: local(path),
thumbnail: None,
disable_content_type_detection: false,
caption,
})
}
}
}
}
async fn public_chat(username: &str, client_id: i32) -> Result<i64, String> {
match functions::search_public_chat(username.to_string(), client_id).await {
Ok(enums::Chat::Chat(chat)) => Ok(chat.id),
Err(e) if e.code == 400 => Err(format!(
"Nobody on Telegram is called @{}",
crate::text::clean(username)
)),
Err(e) => Err(e.message),
}
}
fn reply_to_message(message_id: i64) -> enums::InputMessageReplyTo {
enums::InputMessageReplyTo::Message(types::InputMessageReplyToMessage {
message_id,
quote: None,
checklist_task_id: 0,
})
}
pub fn plain(text: String) -> types::FormattedText {
types::FormattedText {
text,
entities: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quiet() {
let request = json!({ "@type": "setLogVerbosityLevel", "new_verbosity_level": 1 });
execute(&request).unwrap();
}
fn entities(text: &types::FormattedText) -> Vec<(i32, i32, String)> {
text.entities
.iter()
.map(|e| {
let kind = format!("{:?}", e.r#type);
(e.offset, e.length, kind)
})
.collect()
}
#[test]
fn markdown_in_the_composer_is_sent_as_formatting() {
quiet();
let sent =
markdown("**bold** __it__ ~~gone~~ ||secret|| `code` [site](https://x.dev)".into());
assert_eq!(sent.text, "bold it gone secret code site");
assert_eq!(
entities(&sent),
[
(0, 4, "Bold".into()),
(5, 2, "Italic".into()),
(8, 4, "Strikethrough".into()),
(13, 6, "Spoiler".into()),
(20, 4, "Code".into()),
(
25,
4,
"TextUrl(TextEntityTypeTextUrl { url: \"https://x.dev/\" })".into()
),
]
);
}
#[test]
fn a_markdown_link_to_a_user_mentions_them() {
quiet();
let sent = markdown("hi [Bob Smith](tg://user?id=123)".into());
assert_eq!(sent.text, "hi Bob Smith");
assert_eq!(
entities(&sent),
[(
3,
9,
"MentionName(TextEntityTypeMentionName { user_id: 123 })".into()
)]
);
}
#[test]
fn single_stars_and_underscores_stay_as_typed() {
quiet();
let sent = markdown("2*3*4 = 24, snake_case_name, a ** b".into());
assert_eq!(sent.text, "2*3*4 = 24, snake_case_name, a ** b");
assert!(sent.entities.is_empty(), "{:?}", sent.entities);
}
#[test]
fn edits_start_from_the_message_as_markdown() {
quiet();
let entity = |offset, length, r#type| types::TextEntity {
offset,
length,
r#type,
};
let bold = types::FormattedText {
text: "bold and plain".into(),
entities: vec![entity(0, 4, enums::TextEntityType::Bold)],
};
let edit = to_markdown(&bold).unwrap();
assert_eq!(edit.markdown, "**bold** and plain");
assert!(!edit.loses);
assert_eq!(markdown(edit.markdown), bold);
let underlined = types::FormattedText {
text: "under".into(),
entities: vec![entity(0, 5, enums::TextEntityType::Underline)],
};
assert!(
to_markdown(&underlined).unwrap().loses,
"Markdown has no underline"
);
let mention = types::FormattedText {
text: "hi Bob".into(),
entities: vec![entity(
3,
3,
enums::TextEntityType::MentionName(types::TextEntityTypeMentionName {
user_id: 123,
}),
)],
};
let edit = to_markdown(&mention).unwrap();
assert_eq!(edit.markdown, "hi [Bob](tg://user?id=123)");
assert!(!edit.loses);
assert_eq!(markdown(edit.markdown), mention);
}
#[test]
fn openssl_is_told_not_to_read_a_config_file() {
super::no_openssl_config().unwrap();
super::no_openssl_config().unwrap();
}
}