td_client/message.rs
1//! Terminal message-send results and temporary message identities.
2//!
3//! A direct `TDLib` send response can contain a local temporary message. Successful
4//! submission is not necessarily successful delivery. [`Client::track`]
5//! and [`Client::track_all`] bind pending identities before the request wakes,
6//! then wait for send-success, send-failure, or non-cache deletion updates.
7//! Every original update remains available through [`Session::recv`](crate::session::Session::recv).
8//!
9//! # Supported requests
10//!
11//! Use tracked methods for normal sends, such as `sendMessage`,
12//! `sendMessageAlbum`, and normal non-preview `forwardMessages` calls.
13//! Generic return-type bounds are not a promise to track every function returning
14//! `Message` or `Messages`. Previews, getters, and edits belong on
15//! [`Client::send`]; an unsupported pending-looking response may never finish.
16//! Already-final direct responses return without waiting for a terminal update.
17//!
18//! Edits use their direct correlated response. An `updateMessageEdited` event
19//! cannot establish which edit request finished or whether another edit failed.
20//!
21//! # Cancellation
22//!
23//! A borrowed [`CancellationToken`] asks the operation to delete pending temporary
24//! messages. It does not retract the initial request: even a pre-cancelled token
25//! waits for the direct response so the temporary identity can be bound.
26//!
27//! An authoritative success already observed wins and returns the final message.
28//! Deletion is requested only for a temporary ID still registered as pending;
29//! this library never explicitly deletes a successful final ID. This is **not
30//! server-atomic**: `TDLib` can itself delete a concurrently accepted message after
31//! removing its pending record. Do not interpret cancellation as a guarantee that
32//! the message was never visible or that a racing successful message remains.
33//!
34//! Dropping the future merely abandons local observation. Submitted native work
35//! continues; token-triggered cleanup only runs while the future is driven.
36//! Reusing a cancelled token asks every subsequent operation using it to cancel.
37//!
38//! If an application stop signal wins a race, cancel the token and keep awaiting
39//! the same send future so native cleanup can finish:
40//!
41//! ```no_run
42//! # use std::future::Future;
43//! # use td_client::Client;
44//! # use td_client::Result;
45//! # use td_client::CancellationToken;
46//! # use td_types::{fns, types};
47//! # async fn send_until_stop(
48//! # client: &Client, request: &fns::sendMessage, stop: impl Future<Output = ()>,
49//! # ) -> Result<types::message> {
50//! let cancel = CancellationToken::new();
51//! let sending = client.track(request, Some(&cancel), None);
52//! tokio::pin!(sending);
53//! tokio::select! {
54//! result = &mut sending => result,
55//! () = stop => {
56//! cancel.cancel();
57//! sending.await
58//! }
59//! }
60//! # }
61//! ```
62//!
63//! # Upload measurements
64//!
65//! Pass an optional borrowed callback to observe primary media files. Supported
66//! payloads are animations, audio, documents, photos (the last returned size),
67//! stickers, videos, video notes, and voice notes. Thumbnails and recursive
68//! attachment traversal are not tracked. There is no preliminary-upload API.
69//!
70//! Samples can coalesce across album items; neither a callback for every item nor
71//! a final 100% sample is guaranteed. See [`Progress`] and the
72//! [transfer guide](crate::transfer) for the common measurement contract.
73
74use td_types::enums::{Message, Messages};
75use td_types::traits::Function;
76use td_types::types;
77
78use crate::client::Client;
79use crate::connection::tracking::with_progress;
80use crate::error::{Error, Result};
81use crate::transfer::{CancellationToken, Progress};
82
83/// The chat and temporary message ID identifying a tracked send failure.
84///
85/// Message IDs are scoped to a chat, so both fields are required. Keys carried
86/// by [`Error::MessageFailed`] or [`Error::MessageDeleted`] refer to the
87/// temporary send identity, not a replacement successful final message ID.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct MessageKey {
90 /// The chat containing the temporary message.
91 pub chat_id: i64,
92 /// The temporary message ID assigned by `TDLib`.
93 pub message_id: i64,
94}
95
96impl Client {
97 /// Sends one normal message and waits for its terminal result.
98 ///
99 /// Accepts a generated function returning `td-types::enums::Message`, subject
100 /// to the [normal-send contract](crate::message#supported-requests). The result
101 /// is the final concrete message, not merely the initial pending response.
102 /// Internal tracking does not depend on the application receiving updates.
103 ///
104 /// `progress` receives `(0, measurement)`; it runs synchronously on the task
105 /// polling this future and must not block or panic. See [`Progress`] for
106 /// coalescing and unknown totals.
107 ///
108 /// # Errors
109 ///
110 /// Returns direct-request errors, [`Error::MessageFailed`] on terminal send
111 /// failure, or [`Error::MessageDeleted`] on non-cache deletion. Token-driven
112 /// deletion maps to [`Error::Cancelled`]. Native deletion failures are returned
113 /// while the message remains pending; terminal outcomes or teardown can win
114 /// during deletion. Teardown can produce [`Error::Disconnected`]; an unexpected
115 /// direct response produces
116 /// [`Error::UnexpectedResponse`] or [`Error::Json`].
117 ///
118 /// # Cancellation
119 ///
120 /// See the [cancellation contract](crate::message#cancellation). Dropping this
121 /// future performs no native cancellation and does not undo a sent message.
122 ///
123 /// # Examples
124 ///
125 /// ```no_run
126 /// # use td_client::Client;
127 /// # use td_client::Result;
128 /// use td_types::{fns, types};
129 ///
130 /// # async fn greet(client: &Client, chat_id: i64) -> Result {
131 /// let text = types::formattedText { text: "Hello!".into(), ..Default::default() };
132 /// let content = types::inputMessageText { text, ..Default::default() };
133 /// let request = fns::sendMessage {
134 /// chat_id,
135 /// input_message_content: content.into(),
136 /// ..Default::default()
137 /// };
138 /// let message = client.track(&request, None, None).await?;
139 /// println!("Sent message {}", message.id);
140 /// # Ok(())
141 /// # }
142 /// ```
143 pub async fn track<F: Function<Return = Message>>(
144 &self,
145 request: &F,
146 cancel: Option<&CancellationToken>,
147 progress: Option<&mut (dyn FnMut(usize, Progress) + Send)>,
148 ) -> Result<types::message> {
149 let connection = self.connection()?;
150 let batch = connection.messages(request, progress.is_some()).await?;
151 let [message] = batch.pending.try_into().map_err(|_| Error::UnexpectedResponse("expected one message"))?;
152 with_progress(message.finish(&connection, cancel), batch.samples, progress).await
153 }
154
155 /// Sends a normal-message batch and returns individual terminal results.
156 ///
157 /// Accepts a generated function returning `td-types::enums::Messages`, subject
158 /// to the [normal-send contract](crate::message#supported-requests). It is not
159 /// restricted to albums. Results retain direct-response order, not completion
160 /// order; an empty batch returns an empty vector.
161 ///
162 /// The outer result describes submission and direct-response handling. Once
163 /// bound, each message has its own result: one terminal failure does not erase
164 /// successful messages elsewhere in the batch.
165 ///
166 /// `progress` receives the zero-based direct-response item index and a
167 /// measurement. One shared observation channel coalesces samples across items;
168 /// intermediate callbacks for every item are not guaranteed.
169 ///
170 /// # Errors
171 ///
172 /// The outer result reports direct-request, decoding, and pre-binding
173 /// disconnection errors. Each inner result has the terminal/cancellation errors
174 /// documented on [`track`](Self::track).
175 ///
176 /// # Cancellation
177 ///
178 /// One token applies to the whole batch, not one item. Pending items are awaited
179 /// and, when requested, cancelled sequentially in response order; later items
180 /// may finish before their cancellation is attempted. Successful results remain
181 /// successful. There is no all-or-nothing send or rollback guarantee.
182 ///
183 /// Dropping this future abandons observation of the whole batch without native
184 /// cancellation. See the [shared cancellation contract](crate::message#cancellation).
185 ///
186 /// # Examples
187 ///
188 /// Handle partial failure instead of assuming the outer `Ok` means every send
189 /// succeeded:
190 ///
191 /// ```no_run
192 /// # use td_client::Client;
193 /// # use td_client::Result;
194 /// # use td_types::fns;
195 /// # async fn album(client: &Client, request: &fns::sendMessageAlbum) -> Result {
196 /// let results = client.track_all(request, None, None).await?;
197 /// for (index, result) in results.into_iter().enumerate() {
198 /// match result {
199 /// Ok(message) => println!("Item {index}: message {}", message.id),
200 /// Err(error) => eprintln!("Item {index}: {error}"),
201 /// }
202 /// }
203 /// # Ok(())
204 /// # }
205 /// ```
206 pub async fn track_all<F: Function<Return = Messages>>(
207 &self,
208 request: &F,
209 cancel: Option<&CancellationToken>,
210 progress: Option<&mut (dyn FnMut(usize, Progress) + Send)>,
211 ) -> Result<Vec<Result<types::message>>> {
212 let connection = self.connection()?;
213 let batch = connection.messages(request, progress.is_some()).await?;
214 Ok(batch.finish(&connection, cancel, progress).await)
215 }
216}