Skip to main content

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}