|
|
|
|
@@ -0,0 +1,933 @@
|
|
|
|
|
use crate::tdlib::types::ForwardInfo;
|
|
|
|
|
use crate::tdlib::{
|
|
|
|
|
AuthState, ChatInfo, FolderInfo, MediaInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo,
|
|
|
|
|
TdClientTrait,
|
|
|
|
|
};
|
|
|
|
|
use crate::types::{ChatId, MessageId, UserId};
|
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
|
|
|
|
|
|
/// Platform-neutral Telegram session facade for native clients.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct CoreSession<C> {
|
|
|
|
|
client: C,
|
|
|
|
|
events: VecDeque<CoreEvent>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<C> CoreSession<C> {
|
|
|
|
|
pub fn new(client: C) -> Self {
|
|
|
|
|
Self { client, events: VecDeque::new() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn client(&self) -> &C {
|
|
|
|
|
&self.client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn client_mut(&mut self) -> &mut C {
|
|
|
|
|
&mut self.client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn into_client(self) -> C {
|
|
|
|
|
self.client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn enqueue_event(&mut self, event: CoreEvent) {
|
|
|
|
|
self.events.push_back(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn poll_events(&mut self) -> Vec<CoreEvent> {
|
|
|
|
|
self.events.drain(..).collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<C: TdClientTrait> CoreSession<C> {
|
|
|
|
|
pub fn auth_state(&self) -> CoreAuthState {
|
|
|
|
|
CoreAuthState::from(self.client.auth_state())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn network_state(&self) -> CoreNetworkState {
|
|
|
|
|
CoreNetworkState::from(&self.client.network_state())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn emit_auth_state(&mut self) -> CoreAuthState {
|
|
|
|
|
let state = self.auth_state();
|
|
|
|
|
self.enqueue_event(CoreEvent::AuthChanged(state.clone()));
|
|
|
|
|
state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn emit_network_state(&mut self) -> CoreNetworkState {
|
|
|
|
|
let state = self.network_state();
|
|
|
|
|
self.enqueue_event(CoreEvent::NetworkChanged(state.clone()));
|
|
|
|
|
state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_phone_number(&self, phone: String) -> Result<(), String> {
|
|
|
|
|
self.client.send_phone_number(phone).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_code(&self, code: String) -> Result<(), String> {
|
|
|
|
|
self.client.send_code(code).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_password(&self, password: String) -> Result<(), String> {
|
|
|
|
|
self.client.send_password(password).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn load_chats(&mut self, limit: i32) -> Result<Vec<CoreChatSummary>, String> {
|
|
|
|
|
self.client.load_chats(limit).await?;
|
|
|
|
|
let chats = self.chat_summaries();
|
|
|
|
|
self.enqueue_event(CoreEvent::ChatListChanged(chats.clone()));
|
|
|
|
|
Ok(chats)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn load_folder_chats(
|
|
|
|
|
&mut self,
|
|
|
|
|
folder_id: i32,
|
|
|
|
|
limit: i32,
|
|
|
|
|
) -> Result<Vec<CoreChatSummary>, String> {
|
|
|
|
|
self.client.load_folder_chats(folder_id, limit).await?;
|
|
|
|
|
let chats = self.chat_summaries();
|
|
|
|
|
self.enqueue_event(CoreEvent::ChatListChanged(chats.clone()));
|
|
|
|
|
Ok(chats)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn chat_summaries(&self) -> Vec<CoreChatSummary> {
|
|
|
|
|
self.client
|
|
|
|
|
.chats()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(CoreChatSummary::from)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn folders(&self) -> Vec<CoreFolder> {
|
|
|
|
|
self.client.folders().iter().map(CoreFolder::from).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn open_chat_history(
|
|
|
|
|
&mut self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
limit: i32,
|
|
|
|
|
) -> Result<Vec<CoreMessage>, String> {
|
|
|
|
|
self.client.set_current_chat_id(Some(chat_id));
|
|
|
|
|
let messages = self.client.get_chat_history(chat_id, limit).await?;
|
|
|
|
|
let messages = messages.iter().map(CoreMessage::from).collect();
|
|
|
|
|
Ok(messages)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_text_message(
|
|
|
|
|
&mut self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
text: String,
|
|
|
|
|
reply_to_message_id: Option<MessageId>,
|
|
|
|
|
reply: Option<ReplyInfo>,
|
|
|
|
|
) -> Result<CoreMessage, String> {
|
|
|
|
|
let message = self
|
|
|
|
|
.client
|
|
|
|
|
.send_message(chat_id, text, reply_to_message_id, reply)
|
|
|
|
|
.await?;
|
|
|
|
|
let message = CoreMessage::from(&message);
|
|
|
|
|
self.enqueue_event(CoreEvent::MessageAdded { chat_id, message: message.clone() });
|
|
|
|
|
Ok(message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn edit_text_message(
|
|
|
|
|
&mut self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message_id: MessageId,
|
|
|
|
|
text: String,
|
|
|
|
|
) -> Result<CoreMessage, String> {
|
|
|
|
|
let message = self.client.edit_message(chat_id, message_id, text).await?;
|
|
|
|
|
let message = CoreMessage::from(&message);
|
|
|
|
|
self.enqueue_event(CoreEvent::MessageUpdated { chat_id, message: message.clone() });
|
|
|
|
|
Ok(message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn delete_messages(
|
|
|
|
|
&mut self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message_ids: Vec<MessageId>,
|
|
|
|
|
revoke: bool,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
self.client
|
|
|
|
|
.delete_messages(chat_id, message_ids.clone(), revoke)
|
|
|
|
|
.await?;
|
|
|
|
|
self.enqueue_event(CoreEvent::MessageDeleted { chat_id, message_ids });
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn forward_messages(
|
|
|
|
|
&mut self,
|
|
|
|
|
to_chat_id: ChatId,
|
|
|
|
|
from_chat_id: ChatId,
|
|
|
|
|
message_ids: Vec<MessageId>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
self.client
|
|
|
|
|
.forward_messages(to_chat_id, from_chat_id, message_ids)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn toggle_reaction(
|
|
|
|
|
&mut self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message_id: MessageId,
|
|
|
|
|
reaction: String,
|
|
|
|
|
) -> Result<Vec<CoreReaction>, String> {
|
|
|
|
|
self.client
|
|
|
|
|
.toggle_reaction(chat_id, message_id, reaction)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let reactions: Vec<CoreReaction> = self
|
|
|
|
|
.client
|
|
|
|
|
.get_chat_history(chat_id, i32::MAX)
|
|
|
|
|
.await?
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|message| message.id() == message_id)
|
|
|
|
|
.map(|message| message.reactions().iter().map(CoreReaction::from).collect())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
self.enqueue_event(CoreEvent::ReactionChanged {
|
|
|
|
|
chat_id,
|
|
|
|
|
message_id,
|
|
|
|
|
reactions: reactions.clone(),
|
|
|
|
|
});
|
|
|
|
|
Ok(reactions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn download_photo(&self, file_id: i32) -> Result<CoreDownloadedFile, String> {
|
|
|
|
|
self.client
|
|
|
|
|
.download_file(file_id)
|
|
|
|
|
.await
|
|
|
|
|
.map(|path| CoreDownloadedFile { file_id, path })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn download_voice(&self, file_id: i32) -> Result<CoreDownloadedFile, String> {
|
|
|
|
|
self.client
|
|
|
|
|
.download_voice_note(file_id)
|
|
|
|
|
.await
|
|
|
|
|
.map(|path| CoreDownloadedFile { file_id, path })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn search_messages(
|
|
|
|
|
&self,
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
query: &str,
|
|
|
|
|
) -> Result<Vec<CoreSearchResult>, String> {
|
|
|
|
|
let messages = self.client.search_messages(chat_id, query).await?;
|
|
|
|
|
Ok(messages
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|message| CoreSearchResult { chat_id, message: CoreMessage::from(message) })
|
|
|
|
|
.collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn open_profile(&mut self, chat_id: ChatId) -> Result<CoreProfile, String> {
|
|
|
|
|
let profile = self
|
|
|
|
|
.client
|
|
|
|
|
.get_profile_info(chat_id)
|
|
|
|
|
.await
|
|
|
|
|
.map(|profile| CoreProfile::from(&profile))?;
|
|
|
|
|
self.enqueue_event(CoreEvent::ProfileLoaded(profile.clone()));
|
|
|
|
|
Ok(profile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn set_draft(&mut self, chat_id: ChatId, text: String) -> Result<(), String> {
|
|
|
|
|
self.client.set_draft_message(chat_id, text.clone()).await?;
|
|
|
|
|
self.enqueue_event(CoreEvent::DraftChanged(CoreDraft { chat_id, text }));
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn drain_client_events(&mut self) -> Vec<CoreEvent> {
|
|
|
|
|
let events: Vec<_> = self
|
|
|
|
|
.client
|
|
|
|
|
.drain_incoming_message_events()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|event| {
|
|
|
|
|
CoreEvent::IncomingNotificationCandidate(CoreNotificationCandidate {
|
|
|
|
|
chat: CoreChatSummary::from(&event.chat),
|
|
|
|
|
message: CoreMessage::from(&event.message),
|
|
|
|
|
sender_name: event.sender_name,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
self.events.extend(events.iter().cloned());
|
|
|
|
|
events
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreAccount {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub display_name: String,
|
|
|
|
|
pub is_active: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreAuthState {
|
|
|
|
|
WaitTdlibParameters,
|
|
|
|
|
WaitPhoneNumber,
|
|
|
|
|
WaitCode,
|
|
|
|
|
WaitPassword,
|
|
|
|
|
Ready,
|
|
|
|
|
Closed,
|
|
|
|
|
Error { message: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&AuthState> for CoreAuthState {
|
|
|
|
|
fn from(value: &AuthState) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
AuthState::WaitTdlibParameters => Self::WaitTdlibParameters,
|
|
|
|
|
AuthState::WaitPhoneNumber => Self::WaitPhoneNumber,
|
|
|
|
|
AuthState::WaitCode => Self::WaitCode,
|
|
|
|
|
AuthState::WaitPassword => Self::WaitPassword,
|
|
|
|
|
AuthState::Ready => Self::Ready,
|
|
|
|
|
AuthState::Closed => Self::Closed,
|
|
|
|
|
AuthState::Error(message) => Self::Error { message: message.clone() },
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreChatSummary {
|
|
|
|
|
pub id: ChatId,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
pub last_message: String,
|
|
|
|
|
pub last_message_date: i32,
|
|
|
|
|
pub unread_count: i32,
|
|
|
|
|
pub unread_mention_count: i32,
|
|
|
|
|
pub is_pinned: bool,
|
|
|
|
|
pub order: i64,
|
|
|
|
|
pub last_read_outbox_message_id: MessageId,
|
|
|
|
|
pub folder_ids: Vec<i32>,
|
|
|
|
|
pub is_muted: bool,
|
|
|
|
|
pub draft: Option<CoreDraft>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&ChatInfo> for CoreChatSummary {
|
|
|
|
|
fn from(value: &ChatInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: value.id,
|
|
|
|
|
title: value.title.clone(),
|
|
|
|
|
username: value.username.clone(),
|
|
|
|
|
last_message: value.last_message.clone(),
|
|
|
|
|
last_message_date: value.last_message_date,
|
|
|
|
|
unread_count: value.unread_count,
|
|
|
|
|
unread_mention_count: value.unread_mention_count,
|
|
|
|
|
is_pinned: value.is_pinned,
|
|
|
|
|
order: value.order,
|
|
|
|
|
last_read_outbox_message_id: value.last_read_outbox_message_id,
|
|
|
|
|
folder_ids: value.folder_ids.clone(),
|
|
|
|
|
is_muted: value.is_muted,
|
|
|
|
|
draft: value
|
|
|
|
|
.draft_text
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|text| CoreDraft { chat_id: value.id, text: text.clone() }),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreFolder {
|
|
|
|
|
pub id: i32,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&FolderInfo> for CoreFolder {
|
|
|
|
|
fn from(value: &FolderInfo) -> Self {
|
|
|
|
|
Self { id: value.id, name: value.name.clone() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreMessage {
|
|
|
|
|
pub id: MessageId,
|
|
|
|
|
pub sender_name: String,
|
|
|
|
|
pub date: i32,
|
|
|
|
|
pub edit_date: Option<i32>,
|
|
|
|
|
pub media_album_id: Option<i64>,
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub media: Option<CoreMedia>,
|
|
|
|
|
pub is_outgoing: bool,
|
|
|
|
|
pub is_read: bool,
|
|
|
|
|
pub can_be_edited: bool,
|
|
|
|
|
pub can_be_deleted_only_for_self: bool,
|
|
|
|
|
pub can_be_deleted_for_all_users: bool,
|
|
|
|
|
pub reply: Option<CoreReply>,
|
|
|
|
|
pub forward: Option<CoreForward>,
|
|
|
|
|
pub reactions: Vec<CoreReaction>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&MessageInfo> for CoreMessage {
|
|
|
|
|
fn from(value: &MessageInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: value.id(),
|
|
|
|
|
sender_name: value.sender_name().to_string(),
|
|
|
|
|
date: value.date(),
|
|
|
|
|
edit_date: value.is_edited().then_some(value.metadata.edit_date),
|
|
|
|
|
media_album_id: (value.media_album_id() != 0).then_some(value.media_album_id()),
|
|
|
|
|
text: value.text().to_string(),
|
|
|
|
|
media: value.content.media.as_ref().map(CoreMedia::from),
|
|
|
|
|
is_outgoing: value.is_outgoing(),
|
|
|
|
|
is_read: value.is_read(),
|
|
|
|
|
can_be_edited: value.can_be_edited(),
|
|
|
|
|
can_be_deleted_only_for_self: value.can_be_deleted_only_for_self(),
|
|
|
|
|
can_be_deleted_for_all_users: value.can_be_deleted_for_all_users(),
|
|
|
|
|
reply: value.reply_to().map(CoreReply::from),
|
|
|
|
|
forward: value.forward_from().map(CoreForward::from),
|
|
|
|
|
reactions: value.reactions().iter().map(CoreReaction::from).collect(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreReply {
|
|
|
|
|
pub message_id: MessageId,
|
|
|
|
|
pub sender_name: String,
|
|
|
|
|
pub text: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&ReplyInfo> for CoreReply {
|
|
|
|
|
fn from(value: &ReplyInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
message_id: value.message_id,
|
|
|
|
|
sender_name: value.sender_name.clone(),
|
|
|
|
|
text: value.text.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreForward {
|
|
|
|
|
pub sender_name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&ForwardInfo> for CoreForward {
|
|
|
|
|
fn from(value: &ForwardInfo) -> Self {
|
|
|
|
|
Self { sender_name: value.sender_name.clone() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreReaction {
|
|
|
|
|
pub emoji: String,
|
|
|
|
|
pub count: i32,
|
|
|
|
|
pub is_chosen: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&crate::tdlib::types::ReactionInfo> for CoreReaction {
|
|
|
|
|
fn from(value: &crate::tdlib::types::ReactionInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
emoji: value.emoji.clone(),
|
|
|
|
|
count: value.count,
|
|
|
|
|
is_chosen: value.is_chosen,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreMedia {
|
|
|
|
|
Photo(CorePhoto),
|
|
|
|
|
Voice(CoreVoice),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&MediaInfo> for CoreMedia {
|
|
|
|
|
fn from(value: &MediaInfo) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
MediaInfo::Photo(photo) => Self::Photo(CorePhoto {
|
|
|
|
|
file_id: photo.file_id,
|
|
|
|
|
width: photo.width,
|
|
|
|
|
height: photo.height,
|
|
|
|
|
download_state: CoreDownloadState::from(&photo.download_state),
|
|
|
|
|
}),
|
|
|
|
|
MediaInfo::Voice(voice) => Self::Voice(CoreVoice {
|
|
|
|
|
file_id: voice.file_id,
|
|
|
|
|
duration: voice.duration,
|
|
|
|
|
mime_type: voice.mime_type.clone(),
|
|
|
|
|
waveform: voice.waveform.clone(),
|
|
|
|
|
download_state: CoreDownloadState::from(&voice.download_state),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CorePhoto {
|
|
|
|
|
pub file_id: i32,
|
|
|
|
|
pub width: i32,
|
|
|
|
|
pub height: i32,
|
|
|
|
|
pub download_state: CoreDownloadState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreVoice {
|
|
|
|
|
pub file_id: i32,
|
|
|
|
|
pub duration: i32,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
pub waveform: String,
|
|
|
|
|
pub download_state: CoreDownloadState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreDownloadState {
|
|
|
|
|
NotDownloaded,
|
|
|
|
|
Downloading,
|
|
|
|
|
Downloaded { path: String },
|
|
|
|
|
Error { message: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&crate::tdlib::PhotoDownloadState> for CoreDownloadState {
|
|
|
|
|
fn from(value: &crate::tdlib::PhotoDownloadState) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
crate::tdlib::PhotoDownloadState::NotDownloaded => Self::NotDownloaded,
|
|
|
|
|
crate::tdlib::PhotoDownloadState::Downloading => Self::Downloading,
|
|
|
|
|
crate::tdlib::PhotoDownloadState::Downloaded(path) => {
|
|
|
|
|
Self::Downloaded { path: path.clone() }
|
|
|
|
|
}
|
|
|
|
|
crate::tdlib::PhotoDownloadState::Error(message) => {
|
|
|
|
|
Self::Error { message: message.clone() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&crate::tdlib::VoiceDownloadState> for CoreDownloadState {
|
|
|
|
|
fn from(value: &crate::tdlib::VoiceDownloadState) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
crate::tdlib::VoiceDownloadState::NotDownloaded => Self::NotDownloaded,
|
|
|
|
|
crate::tdlib::VoiceDownloadState::Downloading => Self::Downloading,
|
|
|
|
|
crate::tdlib::VoiceDownloadState::Downloaded(path) => {
|
|
|
|
|
Self::Downloaded { path: path.clone() }
|
|
|
|
|
}
|
|
|
|
|
crate::tdlib::VoiceDownloadState::Error(message) => {
|
|
|
|
|
Self::Error { message: message.clone() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreProfile {
|
|
|
|
|
pub chat_id: ChatId,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
pub bio: Option<String>,
|
|
|
|
|
pub phone_number: Option<String>,
|
|
|
|
|
pub chat_type: String,
|
|
|
|
|
pub member_count: Option<i32>,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub invite_link: Option<String>,
|
|
|
|
|
pub is_group: bool,
|
|
|
|
|
pub online_status: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&ProfileInfo> for CoreProfile {
|
|
|
|
|
fn from(value: &ProfileInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
chat_id: value.chat_id,
|
|
|
|
|
title: value.title.clone(),
|
|
|
|
|
username: value.username.clone(),
|
|
|
|
|
bio: value.bio.clone(),
|
|
|
|
|
phone_number: value.phone_number.clone(),
|
|
|
|
|
chat_type: value.chat_type.clone(),
|
|
|
|
|
member_count: value.member_count,
|
|
|
|
|
description: value.description.clone(),
|
|
|
|
|
invite_link: value.invite_link.clone(),
|
|
|
|
|
is_group: value.is_group,
|
|
|
|
|
online_status: value.online_status.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreDraft {
|
|
|
|
|
pub chat_id: ChatId,
|
|
|
|
|
pub text: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreSearchResult {
|
|
|
|
|
pub chat_id: ChatId,
|
|
|
|
|
pub message: CoreMessage,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreDownloadedFile {
|
|
|
|
|
pub file_id: i32,
|
|
|
|
|
pub path: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreNetworkState {
|
|
|
|
|
WaitingForNetwork,
|
|
|
|
|
ConnectingToProxy,
|
|
|
|
|
Connecting,
|
|
|
|
|
Updating,
|
|
|
|
|
Ready,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&NetworkState> for CoreNetworkState {
|
|
|
|
|
fn from(value: &NetworkState) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
NetworkState::WaitingForNetwork => Self::WaitingForNetwork,
|
|
|
|
|
NetworkState::ConnectingToProxy => Self::ConnectingToProxy,
|
|
|
|
|
NetworkState::Connecting => Self::Connecting,
|
|
|
|
|
NetworkState::Updating => Self::Updating,
|
|
|
|
|
NetworkState::Ready => Self::Ready,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreTypingState {
|
|
|
|
|
Idle,
|
|
|
|
|
Typing {
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
user_id: UserId,
|
|
|
|
|
text: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct CoreNotificationCandidate {
|
|
|
|
|
pub chat: CoreChatSummary,
|
|
|
|
|
pub message: CoreMessage,
|
|
|
|
|
pub sender_name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum CoreEvent {
|
|
|
|
|
AuthChanged(CoreAuthState),
|
|
|
|
|
ChatListChanged(Vec<CoreChatSummary>),
|
|
|
|
|
FolderListChanged(Vec<CoreFolder>),
|
|
|
|
|
MessageAdded {
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message: CoreMessage,
|
|
|
|
|
},
|
|
|
|
|
MessageUpdated {
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message: CoreMessage,
|
|
|
|
|
},
|
|
|
|
|
MessageDeleted {
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message_ids: Vec<MessageId>,
|
|
|
|
|
},
|
|
|
|
|
ReactionChanged {
|
|
|
|
|
chat_id: ChatId,
|
|
|
|
|
message_id: MessageId,
|
|
|
|
|
reactions: Vec<CoreReaction>,
|
|
|
|
|
},
|
|
|
|
|
MediaDownloadProgress {
|
|
|
|
|
file_id: i32,
|
|
|
|
|
downloaded_size: i64,
|
|
|
|
|
total_size: i64,
|
|
|
|
|
},
|
|
|
|
|
IncomingNotificationCandidate(CoreNotificationCandidate),
|
|
|
|
|
NetworkChanged(CoreNetworkState),
|
|
|
|
|
TypingChanged(CoreTypingState),
|
|
|
|
|
DraftChanged(CoreDraft),
|
|
|
|
|
ProfileLoaded(CoreProfile),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::tdlib::types::ReactionInfo;
|
|
|
|
|
use crate::tdlib::{
|
|
|
|
|
AuthState, ChatInfo, FolderInfo, MessageBuilder, NetworkState, ProfileInfo,
|
|
|
|
|
};
|
|
|
|
|
use crate::test_support::FakeTdClient;
|
|
|
|
|
use crate::types::{ChatId, MessageId};
|
|
|
|
|
|
|
|
|
|
fn sample_chat() -> ChatInfo {
|
|
|
|
|
ChatInfo {
|
|
|
|
|
id: ChatId::new(42),
|
|
|
|
|
title: "Team".to_string(),
|
|
|
|
|
username: Some("team_chat".to_string()),
|
|
|
|
|
last_message: "Latest".to_string(),
|
|
|
|
|
last_message_date: 1_700_000_000,
|
|
|
|
|
unread_count: 3,
|
|
|
|
|
unread_mention_count: 1,
|
|
|
|
|
is_pinned: true,
|
|
|
|
|
order: 99,
|
|
|
|
|
last_read_outbox_message_id: MessageId::new(7),
|
|
|
|
|
folder_ids: vec![0, 2],
|
|
|
|
|
is_muted: true,
|
|
|
|
|
draft_text: Some("Draft".to_string()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn auth_state_mapping_is_stable_for_swift() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
CoreAuthState::from(&AuthState::WaitPhoneNumber),
|
|
|
|
|
CoreAuthState::WaitPhoneNumber
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(CoreAuthState::from(&AuthState::WaitCode), CoreAuthState::WaitCode);
|
|
|
|
|
assert_eq!(CoreAuthState::from(&AuthState::WaitPassword), CoreAuthState::WaitPassword);
|
|
|
|
|
assert_eq!(CoreAuthState::from(&AuthState::Ready), CoreAuthState::Ready);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
CoreAuthState::from(&AuthState::Error("bad code".to_string())),
|
|
|
|
|
CoreAuthState::Error { message: "bad code".to_string() }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn chat_summary_preserves_ios_relevant_state() {
|
|
|
|
|
let chat = CoreChatSummary::from(&sample_chat());
|
|
|
|
|
|
|
|
|
|
assert_eq!(chat.id, ChatId::new(42));
|
|
|
|
|
assert_eq!(chat.title, "Team");
|
|
|
|
|
assert_eq!(chat.username.as_deref(), Some("team_chat"));
|
|
|
|
|
assert_eq!(chat.last_message, "Latest");
|
|
|
|
|
assert_eq!(chat.unread_count, 3);
|
|
|
|
|
assert_eq!(chat.unread_mention_count, 1);
|
|
|
|
|
assert!(chat.is_pinned);
|
|
|
|
|
assert!(chat.is_muted);
|
|
|
|
|
assert_eq!(chat.folder_ids, vec![0, 2]);
|
|
|
|
|
assert_eq!(chat.draft.as_ref().map(|draft| draft.text.as_str()), Some("Draft"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn message_mapping_preserves_reply_reactions_and_state() {
|
|
|
|
|
let message = MessageBuilder::new(MessageId::new(100))
|
|
|
|
|
.sender_name("Alice")
|
|
|
|
|
.text("Hello")
|
|
|
|
|
.date(1_700_000_001)
|
|
|
|
|
.edit_date(1_700_000_002)
|
|
|
|
|
.reply_to(crate::tdlib::ReplyInfo {
|
|
|
|
|
message_id: MessageId::new(90),
|
|
|
|
|
sender_name: "Bob".to_string(),
|
|
|
|
|
text: "Original".to_string(),
|
|
|
|
|
})
|
|
|
|
|
.reactions(vec![ReactionInfo {
|
|
|
|
|
emoji: "👍".to_string(), count: 2, is_chosen: true
|
|
|
|
|
}])
|
|
|
|
|
.outgoing()
|
|
|
|
|
.read()
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let mapped = CoreMessage::from(&message);
|
|
|
|
|
|
|
|
|
|
assert_eq!(mapped.id, MessageId::new(100));
|
|
|
|
|
assert_eq!(mapped.sender_name, "Alice");
|
|
|
|
|
assert_eq!(mapped.text, "Hello");
|
|
|
|
|
assert!(mapped.is_outgoing);
|
|
|
|
|
assert!(mapped.is_read);
|
|
|
|
|
assert_eq!(mapped.edit_date, Some(1_700_000_002));
|
|
|
|
|
assert_eq!(mapped.reply.as_ref().map(|reply| reply.message_id), Some(MessageId::new(90)));
|
|
|
|
|
assert_eq!(mapped.reactions[0].emoji, "👍");
|
|
|
|
|
assert!(mapped.reactions[0].is_chosen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn session_event_queue_drains_in_fifo_order() {
|
|
|
|
|
let mut session = CoreSession::new(());
|
|
|
|
|
|
|
|
|
|
session.enqueue_event(CoreEvent::AuthChanged(CoreAuthState::WaitCode));
|
|
|
|
|
session.enqueue_event(CoreEvent::NetworkChanged(CoreNetworkState::Ready));
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
session.poll_events(),
|
|
|
|
|
vec![
|
|
|
|
|
CoreEvent::AuthChanged(CoreAuthState::WaitCode),
|
|
|
|
|
CoreEvent::NetworkChanged(CoreNetworkState::Ready),
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert!(session.poll_events().is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn session_drains_incoming_message_events_as_notification_candidates() {
|
|
|
|
|
let chat = sample_chat();
|
|
|
|
|
let client = FakeTdClient::new().with_chat(chat.clone());
|
|
|
|
|
client.simulate_incoming_message(chat.id, "Ping".to_string(), "Alice");
|
|
|
|
|
let mut session = CoreSession::new(client);
|
|
|
|
|
|
|
|
|
|
let events = session.drain_client_events();
|
|
|
|
|
|
|
|
|
|
assert_eq!(events.len(), 1);
|
|
|
|
|
let CoreEvent::IncomingNotificationCandidate(candidate) = &events[0] else {
|
|
|
|
|
panic!("expected incoming notification candidate");
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(candidate.chat.id, chat.id);
|
|
|
|
|
assert_eq!(candidate.message.text, "Ping");
|
|
|
|
|
assert_eq!(candidate.sender_name, "Alice");
|
|
|
|
|
assert_eq!(session.poll_events(), events);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn events_cover_chat_message_profile_and_folder_shapes() {
|
|
|
|
|
let chat = CoreChatSummary::from(&sample_chat());
|
|
|
|
|
let message = CoreMessage::from(
|
|
|
|
|
&MessageBuilder::new(MessageId::new(10))
|
|
|
|
|
.sender_name("Alice")
|
|
|
|
|
.text("Hi")
|
|
|
|
|
.build(),
|
|
|
|
|
);
|
|
|
|
|
let folder = CoreFolder::from(&FolderInfo { id: 2, name: "Work".to_string() });
|
|
|
|
|
let profile = CoreProfile::from(&ProfileInfo {
|
|
|
|
|
chat_id: ChatId::new(42),
|
|
|
|
|
title: "Team".to_string(),
|
|
|
|
|
username: Some("team_chat".to_string()),
|
|
|
|
|
bio: None,
|
|
|
|
|
phone_number: None,
|
|
|
|
|
chat_type: "Group".to_string(),
|
|
|
|
|
member_count: Some(10),
|
|
|
|
|
description: Some("Project group".to_string()),
|
|
|
|
|
invite_link: None,
|
|
|
|
|
is_group: true,
|
|
|
|
|
online_status: None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
CoreEvent::ChatListChanged(vec![chat.clone()]),
|
|
|
|
|
CoreEvent::ChatListChanged(vec![chat])
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
CoreEvent::MessageAdded { chat_id: ChatId::new(42), message: message.clone() },
|
|
|
|
|
CoreEvent::MessageAdded { chat_id: ChatId::new(42), message }
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(folder.name, "Work");
|
|
|
|
|
assert_eq!(profile.member_count, Some(10));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
CoreNetworkState::from(&NetworkState::WaitingForNetwork),
|
|
|
|
|
CoreNetworkState::WaitingForNetwork
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn facade_methods_enqueue_state_profile_and_draft_events() {
|
|
|
|
|
let profile = ProfileInfo {
|
|
|
|
|
chat_id: ChatId::new(42),
|
|
|
|
|
title: "Team".to_string(),
|
|
|
|
|
username: Some("team_chat".to_string()),
|
|
|
|
|
bio: None,
|
|
|
|
|
phone_number: None,
|
|
|
|
|
chat_type: "Group".to_string(),
|
|
|
|
|
member_count: Some(10),
|
|
|
|
|
description: None,
|
|
|
|
|
invite_link: None,
|
|
|
|
|
is_group: true,
|
|
|
|
|
online_status: None,
|
|
|
|
|
};
|
|
|
|
|
let client = FakeTdClient::new()
|
|
|
|
|
.with_auth_state(AuthState::WaitPassword)
|
|
|
|
|
.with_network_state(NetworkState::Connecting)
|
|
|
|
|
.with_profile(42, profile);
|
|
|
|
|
let mut session = CoreSession::new(client);
|
|
|
|
|
|
|
|
|
|
session.emit_auth_state();
|
|
|
|
|
session.emit_network_state();
|
|
|
|
|
let loaded_profile = session.open_profile(ChatId::new(42)).await.unwrap();
|
|
|
|
|
session
|
|
|
|
|
.set_draft(ChatId::new(42), "Later".to_string())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(loaded_profile.title, "Team");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
session.poll_events(),
|
|
|
|
|
vec![
|
|
|
|
|
CoreEvent::AuthChanged(CoreAuthState::WaitPassword),
|
|
|
|
|
CoreEvent::NetworkChanged(CoreNetworkState::Connecting),
|
|
|
|
|
CoreEvent::ProfileLoaded(CoreProfile {
|
|
|
|
|
chat_id: ChatId::new(42),
|
|
|
|
|
title: "Team".to_string(),
|
|
|
|
|
username: Some("team_chat".to_string()),
|
|
|
|
|
bio: None,
|
|
|
|
|
phone_number: None,
|
|
|
|
|
chat_type: "Group".to_string(),
|
|
|
|
|
member_count: Some(10),
|
|
|
|
|
description: None,
|
|
|
|
|
invite_link: None,
|
|
|
|
|
is_group: true,
|
|
|
|
|
online_status: None,
|
|
|
|
|
}),
|
|
|
|
|
CoreEvent::DraftChanged(CoreDraft {
|
|
|
|
|
chat_id: ChatId::new(42),
|
|
|
|
|
text: "Later".to_string(),
|
|
|
|
|
}),
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn message_mutations_return_models_and_enqueue_events() {
|
|
|
|
|
let chat_id = ChatId::new(42);
|
|
|
|
|
let original = MessageBuilder::new(MessageId::new(10))
|
|
|
|
|
.sender_name("Me")
|
|
|
|
|
.text("Before")
|
|
|
|
|
.outgoing()
|
|
|
|
|
.build();
|
|
|
|
|
let client = FakeTdClient::new()
|
|
|
|
|
.with_chat(sample_chat())
|
|
|
|
|
.with_message(chat_id.as_i64(), original);
|
|
|
|
|
let mut session = CoreSession::new(client);
|
|
|
|
|
|
|
|
|
|
let sent = session
|
|
|
|
|
.send_text_message(chat_id, "Hello".to_string(), None, None)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let edited = session
|
|
|
|
|
.edit_text_message(chat_id, MessageId::new(10), "After".to_string())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
session
|
|
|
|
|
.delete_messages(chat_id, vec![MessageId::new(10)], true)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(sent.text, "Hello");
|
|
|
|
|
assert_eq!(edited.text, "After");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
session.poll_events(),
|
|
|
|
|
vec![
|
|
|
|
|
CoreEvent::MessageAdded { chat_id, message: sent },
|
|
|
|
|
CoreEvent::MessageUpdated { chat_id, message: edited },
|
|
|
|
|
CoreEvent::MessageDeleted { chat_id, message_ids: vec![MessageId::new(10)] },
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn facade_delegates_auth_forward_reactions_and_downloads() {
|
|
|
|
|
let chat_id = ChatId::new(42);
|
|
|
|
|
let other_chat_id = ChatId::new(100);
|
|
|
|
|
let message = MessageBuilder::new(MessageId::new(10))
|
|
|
|
|
.sender_name("Alice")
|
|
|
|
|
.text("React here")
|
|
|
|
|
.build();
|
|
|
|
|
let client = FakeTdClient::new()
|
|
|
|
|
.with_message(chat_id.as_i64(), message)
|
|
|
|
|
.with_downloaded_file(77, "/tmp/photo.jpg");
|
|
|
|
|
let mut session = CoreSession::new(client);
|
|
|
|
|
|
|
|
|
|
session
|
|
|
|
|
.send_phone_number("+10000000000".to_string())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
session.send_code("12345".to_string()).await.unwrap();
|
|
|
|
|
session.send_password("secret".to_string()).await.unwrap();
|
|
|
|
|
session
|
|
|
|
|
.forward_messages(other_chat_id, chat_id, vec![MessageId::new(10)])
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let reactions = session
|
|
|
|
|
.toggle_reaction(chat_id, MessageId::new(10), "👍".to_string())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let downloaded = session.download_photo(77).await.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(downloaded.path, "/tmp/photo.jpg");
|
|
|
|
|
assert_eq!(session.client().get_forwarded_messages().len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
reactions,
|
|
|
|
|
vec![CoreReaction {
|
|
|
|
|
emoji: "👍".to_string(), count: 1, is_chosen: true
|
|
|
|
|
}]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
session.poll_events(),
|
|
|
|
|
vec![CoreEvent::ReactionChanged { chat_id, message_id: MessageId::new(10), reactions }]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|