Skip to main content

simploxide_client/
messages.rs

1//! Message builders.
2//!
3//! Any [`MessageLike`] value can be passed to send message methods and then modified by a
4//! plenty of builder options as shown in the usage examples below
5//!
6//!
7//! ### Simple text
8//!
9//! ```ignore
10//! // Regular text message
11//! bot.send_msg(chat, "Hello").await?;
12//!
13//! // Regular text reply with TTL
14//! bot.send_msg(chat, "Hello")
15//!     .reply_to(msg)
16//!     .with_ttl(Duration::from_secs(3600))
17//!     .await?;
18//!
19//! // Formatted text
20//! bot.send_msg(chat, Text::yellow("Warning: operation is cancelledd")).await?;
21//!
22//! // Heavily-formatted text
23//! bot.send_msg(
24//!     chat,
25//!     format!("{}\n\nThe operation {} {}",
26//!         "Attention".bold(),
27//!         op.italic(),
28//!         "is not permitted".red()
29//!     )
30//! ).await?;
31//! ```
32//!
33//! ### Simple files
34//!
35//! ```ignore
36//! // Plain file with caption
37//! bot.send_msg(
38//!     chat,
39//!     File::new("document.pdf")
40//!         .with_caption("Here's the doc")
41//! ).await?;
42//!
43//! // Same as above but with a message builder method
44//! bot.send_msg(chat, File::new("document.pdf"))
45//!    .set_text("Here's the doc")
46//!    .await?;
47//!
48//! // Attach a CryptoFile to a text message
49//! bot.send_msg(chat, "See attached")
50//!    .attach(crypto_file)
51//!    .await?;
52//! ```
53//!
54//! ### Images
55//!
56//! ```ignore
57//! // With multimedia: source file is automatically transcoded into a thumbnail
58//! // Without multimedia: sends with the default placeholder as a preview
59//! bot.send_msg(chat, Image::new("img.jpg")).await?;
60//!
61//! // Override transcoder settings(requires `multimedia` feature)
62//! bot.send_msg(chat, Image::new("img.jpg"))
63//!     .with_transcoder(
64//!         Transcoder::default()
65//!             .with_size(200, 200)
66//!             .with_quality(80)
67//!             .with_blur(1.5)
68//!     ).await?;
69//!
70//! // Get thumbnail from in memory bytes. With `multimedia` feature the bytes will be transcoded
71//! // to JPG so with_transcoder(Transcoder::disabled()) is used to opt out, without multimedia the bytes
72//! // are used as is
73//! bot.send_msg(chat, Image::new("img.jpg"))
74//!     .with_preview(
75//!         ImagePreview::from_bytes(thumb_bytes)
76//!             .with_transcoder(Transcoder::disabled())
77//!     ).await?;
78//!
79//! // Thumbnail from a separate file(read asyncronously at send time)
80//! bot.send_msg(chat, Image::new("img.jpg"))
81//!     .with_preview(ImagePreview::from_file("thumb.jpg"))
82//!     .await?;
83//!
84//! // Encrypted source and thumbnail(requires feature `native_crypto`)
85//! bot.send_msg(chat, Image::from(image_crypto_file))
86//!     .with_preview(ImagePreview::from_crypto_file(thumb_crypto_file))
87//!     .await?;
88//!
89//! // Text transitioning to image so "Here is the photo" becomes the caption
90//! bot.send_msg(chat, "Here is the photo")
91//!     .with_image(Image::new("img.jpg"))
92//!     .await?;
93//! ```
94//!
95//! ### Video
96//!
97//! Automatic preview generation from video files is currently unsupported. A custom preview can be
98//! provided, or the message sends with the default placeholder preview.
99//!
100//! ```ignore
101//! // Default placeholder preview
102//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30))).await?;
103//!
104//! // Custom thumbnail
105//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30)))
106//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
107//!     .await?;
108//!
109//! // Custom thumbnail from a file, resized at send time(requires `multimedia`)
110//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30)))
111//!     .with_preview(
112//!         ImagePreview::from_file("thumb.jpg")
113//!             .with_transcoder(Transcoder::default().with_size(255, 255))
114//!     )
115//!     .await?;
116//! ```
117//!
118//! ### Link
119//!
120//! ```ignore
121//! // Minimal: no preview image, no metadata
122//! bot.send_msg(chat, Link::new("https://example.com")).await?;
123//!
124//! // Full Open Graph preview
125//! let og_bytes: Vec<u8> = fetch_og_image("https://example.com").await?;
126//! bot.send_msg(chat,
127//!     Link::new("https://example.com")
128//!         .with_title("Example Domain")
129//!         .with_description("Domain description")
130//!         .with_content(LinkContent::make_page())
131//! )
132//! .with_preview(ImagePreview::from_bytes(og_bytes))
133//! .await?;
134//!
135//! // Text transitioning to link
136//! bot.send_msg(chat, "Check this out")
137//!     .with_link(Link::new("https://example.com").with_title("Example"))
138//!     .await?;
139//! ```
140//!
141//! ### Special messages like reports and chat links
142//!
143//! ```ignore
144//! // Report
145//! bot.send_msg(chat, Report::spam("Unsolicited advertisement")).await?;
146//!
147//! // Report via text transition so the text becomes the report body
148//! bot.send_msg(chat, "Unsolicited advertisement").report(ReportReason::Spam).await?;
149//!
150//! // Chat invitation
151//! bot.send_msg(chat, Chat::new(chat_link).with_text("Join our group")).await?;
152//! ```
153//!
154//! ### Custom and Raw messages
155//!
156//! Custom messages are useful for implementing interbot protocols
157//!
158//! ```ignore
159//! bot.send_msg(chat, Custom::new("app.ping", &PingPayload { id: 42 })).await?;
160//! ```
161//!
162//! [`ComposedMessage`] is for dynamic construction scenarios where the message content, media
163//! type, or delivery options are determined by program logic rather than known at compile time.
164//! Because [`ComposedMessage`] is sent verbatim, preview resolution is the caller's
165//! responsibility.
166//!
167//! ```ignore
168//! // resolve() always returns a valid preview string, falling back to the default on any error
169//! let preview = ImagePreview::from_file("thumb.jpg").resolve().await;
170//!
171//! // try_resolve() surfaces the error so the caller can dechate what to do
172//! let preview = match ImagePreview::from_file("thumb.jpg").try_resolve().await {
173//!     Ok(s) => s,
174//!     Err(e) => {
175//!         log::error!("Preview failed: {e}");
176//!         return Err(e.into());
177//!     }
178//! };
179//!
180//! let mut msg = ComposedMessage {
181//!     file_source: None,
182//!     msg_content: MsgContent::make_text(String::new()),
183//!     quoted_item_id: None,
184//!     mentions: Default::default(),
185//!     undocumented: Default::default(),
186//! };
187//!
188//! if let Some(image_file) = attachment {
189//!     msg.file_source = Some(image_file);
190//!     msg.msg_content = MsgContent::make_image(caption, preview);
191//! }
192//!
193//! if let Some(id) = reply_to_id {
194//!     msg.quoted_item_id = Some(id);
195//! }
196//!
197//! bot.send_msg(chat, msg).await?;
198//! ```
199//!
200//! ### Broadcasts & Multicasts
201//!
202//! `prepare_broadcast` fetches the recipient list asynchronously, then returns a
203//! `MulticastBuilder`. Preview is resolved **only once** and the result is cloned for every
204//! recipient.
205//!
206//! ```ignore
207//! // All known chats
208//! bot.prepare_broadcast("Hello everyone")
209//!     .await?
210//!     .send()
211//!     .await;
212//!
213//! // Filtered to direct chats only
214//! bot.prepare_broadcast_with("Hello", |id| id.is_direct())
215//!     .await?
216//!     .send()
217//!     .await;
218//!
219//! // Image preview is transcoded/resolved once, result broadcast to all groups
220//! bot.prepare_broadcast_with(Image::new("img.jpg"), |id| id.is_group())
221//!     .await?
222//!     .send()
223//!     .await;
224//!
225//! // Image with in-memory thumbnail
226//! bot.prepare_broadcast(Image::new("img.jpg"))
227//!     .await?
228//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
229//!     .send()
230//!     .await;
231//!
232//! // Text transitioning to link inside the broadcast builder
233//! bot.prepare_broadcast("Check this out")
234//!     .await?
235//!     .with_link(Link::new("https://example.com").with_title("Example"))
236//!     .with_preview(ImagePreview::from_bytes(og_bytes))
237//!     .with_ttl(Duration::from_secs(86400))
238//!     .send()
239//!     .await;
240//!
241//! // Explicit set of chat IDs
242//! bot.multicast_message(chat_ids, Image::new("/tmp/photo.jpg"))
243//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
244//!     .await;
245//! ```
246
247use serde::Serialize;
248use simploxide_api_types::{
249    ComposedMessage, CryptoFile, CryptoFileArgs, JsonObject, LinkContent, LinkOwnerSig,
250    LinkPreview, MsgChatLink, MsgContent, ReportReason, client_api::ClientApi,
251    commands::ApiSendMessages, responses::NewChatItemsResponse,
252};
253
254#[cfg(feature = "multimedia")]
255use crate::preview;
256use crate::{
257    id::{ChatId, MessageId},
258    preferences,
259    preview::{ImagePreview, PreviewKind},
260};
261
262use std::{path::Path, pin::Pin, sync::Arc, time::Duration};
263
264/// A kind for simple text messsages
265pub struct TextKind;
266
267/// A kind for complex messages(simple attachments, reports, etc) that don't require any
268/// pre-processing to be sent
269pub struct RichKind;
270
271/// Builder kind for [`ComposedMessage`]. Content is sent verbatim so no builder methods are
272/// available for this kind.
273pub struct RawKind;
274
275/// Builder kind for messages requiring preview processing. Exposes `with_preview` to override the
276/// thumbnail. With the `multimedia` feature, also exposes `with_transcoder` to control JPEG
277/// re-encoding at send time.
278pub struct PreviewableKind(ImagePreview);
279
280pub trait MessageLike {
281    type Kind;
282    fn into_builder_parts(self) -> (ComposedMessage, Self::Kind);
283}
284
285impl MessageLike for ComposedMessage {
286    type Kind = RawKind;
287    fn into_builder_parts(self) -> (ComposedMessage, RawKind) {
288        (self, RawKind)
289    }
290}
291
292impl MessageLike for MsgContent {
293    type Kind = RichKind;
294    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
295        (wrap_content(self), RichKind)
296    }
297}
298
299impl MessageLike for String {
300    type Kind = TextKind;
301    fn into_builder_parts(self) -> (ComposedMessage, TextKind) {
302        (wrap_content(MsgContent::make_text(self)), TextKind)
303    }
304}
305
306impl MessageLike for &str {
307    type Kind = TextKind;
308    fn into_builder_parts(self) -> (ComposedMessage, TextKind) {
309        self.to_owned().into_builder_parts()
310    }
311}
312
313/// Represents a styled text(applies SimpleX-Chat markdown syntax to the given substr)
314#[derive(Debug, Clone)]
315pub enum Text<'a> {
316    Bold(&'a str),
317    Italic(&'a str),
318    Strike(&'a str),
319    Monospace(&'a str),
320    Secret(&'a str),
321    Red(&'a str),
322    Green(&'a str),
323    Blue(&'a str),
324    Yellow(&'a str),
325    Cyan(&'a str),
326    Magenta(&'a str),
327}
328
329impl std::fmt::Display for Text<'_> {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        let (start, text, end) = match self {
332            Self::Bold(s) => ("*", s, "*"),
333            Self::Italic(s) => ("_", s, "_"),
334            Self::Strike(s) => ("~", s, "~"),
335            Self::Monospace(s) => ("`", s, "`"),
336            Self::Secret(s) => ("#", s, "#"),
337            Self::Red(s) => ("!1 ", s, "!"),
338            Self::Green(s) => ("!2 ", s, "!"),
339            Self::Blue(s) => ("!3, ", s, "!"),
340            Self::Yellow(s) => ("!4 ", s, "!"),
341            Self::Cyan(s) => ("!5 ", s, "!"),
342            Self::Magenta(s) => ("!6 ", s, "!"),
343        };
344
345        for line in text.lines() {
346            if line.trim().is_empty() {
347                writeln!(f, "{line}")?;
348            } else {
349                writeln!(f, "{start}{}{end}", line.trim())?;
350            }
351        }
352
353        Ok(())
354    }
355}
356
357/// An extension trait supposed to construct [`Text`] types from string like types, e.g.
358///
359/// ```ignore
360/// format!("Hello, {}", user_name.bold())
361/// ```
362pub trait TextExt {
363    fn bold(&self) -> Text<'_>;
364    fn italic(&self) -> Text<'_>;
365    fn strike(&self) -> Text<'_>;
366    fn monospace(&self) -> Text<'_>;
367    fn secret(&self) -> Text<'_>;
368    fn red(&self) -> Text<'_>;
369    fn green(&self) -> Text<'_>;
370    fn blue(&self) -> Text<'_>;
371    fn yellow(&self) -> Text<'_>;
372    fn cyan(&self) -> Text<'_>;
373    fn magenta(&self) -> Text<'_>;
374}
375
376impl<S> TextExt for S
377where
378    S: std::ops::Deref<Target = str>,
379{
380    fn bold(&self) -> Text<'_> {
381        Text::Bold(self)
382    }
383
384    fn italic(&self) -> Text<'_> {
385        Text::Italic(self)
386    }
387
388    fn strike(&self) -> Text<'_> {
389        Text::Strike(self)
390    }
391
392    fn monospace(&self) -> Text<'_> {
393        Text::Monospace(self)
394    }
395
396    fn secret(&self) -> Text<'_> {
397        Text::Secret(self)
398    }
399
400    fn red(&self) -> Text<'_> {
401        Text::Red(self)
402    }
403
404    fn green(&self) -> Text<'_> {
405        Text::Green(self)
406    }
407
408    fn blue(&self) -> Text<'_> {
409        Text::Blue(self)
410    }
411
412    fn yellow(&self) -> Text<'_> {
413        Text::Yellow(self)
414    }
415
416    fn cyan(&self) -> Text<'_> {
417        Text::Cyan(self)
418    }
419
420    fn magenta(&self) -> Text<'_> {
421        Text::Magenta(self)
422    }
423}
424
425impl MessageLike for Text<'_> {
426    type Kind = TextKind;
427
428    fn into_builder_parts(self) -> (ComposedMessage, Self::Kind) {
429        self.to_string().into_builder_parts()
430    }
431}
432
433impl MessageLike for CryptoFile {
434    type Kind = RichKind;
435    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
436        (
437            ComposedMessage {
438                file_source: Some(self),
439                msg_content: MsgContent::make_file(String::new()),
440                quoted_item_id: None,
441                mentions: Default::default(),
442                undocumented: Default::default(),
443            },
444            RichKind,
445        )
446    }
447}
448
449/// Image message type. With the `multimedia` feature, auto-transcodes the source file into a
450/// thumbnail on resolve when no explicit preview is set. Without it, the gray placeholder is used.
451/// With `native_crypto` feature can auto-transcode thumbnails even from the encrypted source files
452#[derive(Debug, Clone)]
453pub struct Image {
454    source: CryptoFile,
455    custom_preview: ImagePreview,
456    text: String,
457}
458
459impl Image {
460    pub fn new(path: impl AsRef<Path>) -> Self {
461        Self {
462            source: CryptoFile {
463                file_path: path.as_ref().display().to_string(),
464                crypto_args: None,
465                undocumented: Default::default(),
466            },
467            custom_preview: ImagePreview::default(),
468            text: String::new(),
469        }
470    }
471
472    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
473        self.text = caption.into();
474        self
475    }
476
477    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
478        self.custom_preview = preview;
479        self
480    }
481
482    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
483        self.source.crypto_args = Some(args);
484        self
485    }
486}
487
488impl From<CryptoFile> for Image {
489    fn from(source: CryptoFile) -> Self {
490        Self {
491            source,
492            custom_preview: ImagePreview::default(),
493            text: String::new(),
494        }
495    }
496}
497
498impl MessageLike for Image {
499    type Kind = PreviewableKind;
500    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
501        let preview = if self.custom_preview.kind() != PreviewKind::Default {
502            self.custom_preview
503        } else {
504            make_image_preview(&self.source)
505        };
506
507        (
508            ComposedMessage {
509                file_source: Some(self.source),
510                msg_content: MsgContent::make_image(self.text, String::new()),
511                quoted_item_id: None,
512                mentions: Default::default(),
513                undocumented: Default::default(),
514            },
515            PreviewableKind(preview),
516        )
517    }
518}
519
520#[cfg(all(feature = "multimedia", feature = "native_crypto"))]
521fn make_image_preview(file: &CryptoFile) -> ImagePreview {
522    ImagePreview::from_crypto_file(file.clone())
523}
524
525#[cfg(all(feature = "multimedia", not(feature = "native_crypto")))]
526fn make_image_preview(file: &CryptoFile) -> ImagePreview {
527    if file.crypto_args.is_none() {
528        ImagePreview::from_file(&file.file_path)
529    } else {
530        ImagePreview::default()
531    }
532}
533
534#[cfg(not(feature = "multimedia"))]
535fn make_image_preview(_: &CryptoFile) -> ImagePreview {
536    ImagePreview::default()
537}
538
539/// Video message type. Automatic preview generation from video files is unsupported; set a preview
540/// explicitly or the default placeholder is used. Your app can generate video previews by calling
541/// the external `ffmpeg` process or similar.
542#[derive(Debug, Clone)]
543pub struct Video {
544    source: CryptoFile,
545    preview: ImagePreview,
546    text: String,
547    duration: Duration,
548}
549
550impl Video {
551    pub fn new(path: impl AsRef<Path>, duration: Duration) -> Self {
552        Self {
553            source: CryptoFile {
554                file_path: path.as_ref().display().to_string(),
555                crypto_args: None,
556                undocumented: Default::default(),
557            },
558            preview: ImagePreview::default(),
559            text: String::new(),
560            duration,
561        }
562    }
563
564    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
565        self.text = caption.into();
566        self
567    }
568
569    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
570        self.preview = preview;
571        self
572    }
573
574    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
575        self.source.crypto_args = Some(args);
576        self
577    }
578}
579
580impl From<CryptoFile> for Video {
581    fn from(source: CryptoFile) -> Self {
582        Self {
583            source,
584            preview: ImagePreview::default(),
585            text: String::new(),
586            duration: Duration::ZERO,
587        }
588    }
589}
590
591impl MessageLike for Video {
592    type Kind = PreviewableKind;
593    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
594        (
595            ComposedMessage {
596                file_source: Some(self.source),
597                msg_content: MsgContent::make_video(
598                    self.text,
599                    String::default(),
600                    self.duration.as_secs().try_into().unwrap_or(i32::MAX),
601                ),
602                quoted_item_id: None,
603                mentions: Default::default(),
604                undocumented: Default::default(),
605            },
606            PreviewableKind(self.preview),
607        )
608    }
609}
610
611/// Link preview message. Use `with_title`, `with_description`, and `with_image` to populate
612/// the Open Graph-style card shown to the recipient.
613#[derive(Debug, Clone)]
614pub struct Link {
615    uri: String,
616    title: String,
617    description: String,
618    image: ImagePreview,
619    content: Option<LinkContent>,
620    text: String,
621}
622
623impl Link {
624    pub fn new(uri: impl Into<String>) -> Self {
625        Self {
626            uri: uri.into(),
627            title: String::new(),
628            description: String::new(),
629            image: ImagePreview::default(),
630            content: None,
631            text: String::new(),
632        }
633    }
634
635    pub fn with_title(mut self, title: impl Into<String>) -> Self {
636        self.title = title.into();
637        self
638    }
639
640    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
641        self.description = desc.into();
642        self
643    }
644
645    pub fn with_image(mut self, image: ImagePreview) -> Self {
646        self.image = image;
647        self
648    }
649
650    pub fn with_content(mut self, content: LinkContent) -> Self {
651        self.content = Some(content);
652        self
653    }
654
655    pub fn with_text(mut self, text: impl Into<String>) -> Self {
656        self.text = text.into();
657        self
658    }
659}
660
661impl MessageLike for Link {
662    type Kind = PreviewableKind;
663    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
664        (
665            ComposedMessage {
666                file_source: None,
667                msg_content: MsgContent::make_link(
668                    self.text,
669                    LinkPreview {
670                        uri: self.uri,
671                        title: self.title,
672                        description: self.description,
673                        image: String::new(),
674                        content: self.content,
675                        undocumented: Default::default(),
676                    },
677                ),
678                quoted_item_id: None,
679                mentions: Default::default(),
680                undocumented: Default::default(),
681            },
682            PreviewableKind(self.image),
683        )
684    }
685}
686
687/// Simple file attachment
688#[derive(Debug, Clone)]
689pub struct File {
690    pub text: String,
691    pub file: CryptoFile,
692}
693
694impl File {
695    pub fn new<P: AsRef<Path>>(path: P) -> Self {
696        Self {
697            file: CryptoFile {
698                file_path: path.as_ref().display().to_string(),
699                crypto_args: None,
700                undocumented: Default::default(),
701            },
702            text: String::new(),
703        }
704    }
705
706    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
707        self.text = caption.into();
708        self
709    }
710
711    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
712        self.file.crypto_args = Some(args);
713        self
714    }
715}
716
717impl MessageLike for File {
718    type Kind = RichKind;
719    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
720        (
721            ComposedMessage {
722                file_source: Some(self.file),
723                msg_content: MsgContent::make_file(self.text),
724                quoted_item_id: None,
725                mentions: Default::default(),
726                undocumented: Default::default(),
727            },
728            RichKind,
729        )
730    }
731}
732
733/// A message sent to groups to report other users
734#[derive(Debug, Clone)]
735pub struct Report {
736    pub text: String,
737    pub reason: ReportReason,
738}
739
740impl Report {
741    pub fn spam<S: Into<String>>(text: S) -> Self {
742        Self {
743            text: text.into(),
744            reason: ReportReason::Spam,
745        }
746    }
747
748    pub fn content<S: Into<String>>(text: S) -> Self {
749        Self {
750            text: text.into(),
751            reason: ReportReason::Content,
752        }
753    }
754
755    pub fn community<S: Into<String>>(text: S) -> Self {
756        Self {
757            text: text.into(),
758            reason: ReportReason::Community,
759        }
760    }
761
762    pub fn profile<S: Into<String>>(text: S) -> Self {
763        Self {
764            text: text.into(),
765            reason: ReportReason::Profile,
766        }
767    }
768
769    pub fn other<S: Into<String>>(text: S) -> Self {
770        Self {
771            text: text.into(),
772            reason: ReportReason::Other,
773        }
774    }
775}
776
777impl MessageLike for Report {
778    type Kind = RichKind;
779    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
780        (
781            wrap_content(MsgContent::make_report(self.text, self.reason)),
782            RichKind,
783        )
784    }
785}
786
787impl MessageLike for ReportReason {
788    type Kind = RichKind;
789    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
790        Report {
791            text: String::new(),
792            reason: self,
793        }
794        .into_builder_parts()
795    }
796}
797
798/// Chat invitation message containing a link to a group or direct contact.
799#[derive(Debug, Clone)]
800pub struct Chat {
801    pub text: String,
802    pub link: MsgChatLink,
803    pub owner_sig: Option<LinkOwnerSig>,
804}
805
806impl Chat {
807    pub fn new(link: MsgChatLink) -> Self {
808        Self {
809            text: String::new(),
810            link,
811            owner_sig: None,
812        }
813    }
814
815    pub fn with_text(mut self, text: impl Into<String>) -> Self {
816        self.text = text.into();
817        self
818    }
819
820    pub fn with_owner_sig(mut self, sig: LinkOwnerSig) -> Self {
821        self.owner_sig = Some(sig);
822        self
823    }
824}
825
826impl MessageLike for Chat {
827    type Kind = RichKind;
828    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
829        (
830            wrap_content(MsgContent::make_chat(self.text, self.link, self.owner_sig)),
831            RichKind,
832        )
833    }
834}
835
836/// Application defined message with a string tag and arbitrary JSON payload.
837#[derive(Debug, Clone)]
838pub struct Custom {
839    pub tag: String,
840    pub text: String,
841    pub json: JsonObject,
842}
843
844impl Custom {
845    pub fn new(tag: impl Into<String>, object: impl Serialize) -> Self {
846        // TODO: handle serialize error
847        Self::from_raw(tag.into(), serde_json::to_value(object).unwrap())
848    }
849
850    pub fn from_raw(tag: String, json: JsonObject) -> Self {
851        Self {
852            tag,
853            text: String::new(),
854            json,
855        }
856    }
857
858    pub fn with_text(mut self, text: impl Into<String>) -> Self {
859        self.text = text.into();
860        self
861    }
862}
863
864impl MessageLike for Custom {
865    type Kind = RichKind;
866    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
867        (
868            wrap_content(MsgContent::make_unknown(self.tag, self.text, self.json)),
869            RichKind,
870        )
871    }
872}
873
874/// An awaitable message builder(await sends the message)
875pub struct MessageBuilder<'a, C: 'a + ?Sized, M = TextKind> {
876    pub(crate) client: &'a C,
877    pub(crate) chat_id: ChatId,
878    pub(crate) live: bool,
879    pub(crate) ttl: Option<Duration>,
880    pub(crate) msg: ComposedMessage,
881    pub(crate) kind: M,
882}
883
884impl<'a, C, M> MessageBuilder<'a, C, M> {
885    pub fn live_message(mut self) -> Self {
886        self.live = true;
887        self
888    }
889
890    pub fn with_ttl(mut self, ttl: Duration) -> Self {
891        self.ttl = Some(ttl);
892        self
893    }
894
895    pub fn reply_to(mut self, msg_id: impl Into<MessageId>) -> Self {
896        self.msg.quoted_item_id = Some(msg_id.into().0);
897        self
898    }
899
900    pub fn set_text(mut self, text: impl Into<String>) -> Self {
901        self.msg.msg_content.set_text_part(text);
902        self
903    }
904
905    /// A syntactic sugar to avoid double awaits(`.await.await` -> `.await.send().await`) in
906    /// certain use-cases
907    pub fn send(self) -> <Self as IntoFuture>::IntoFuture
908    where
909        Self: IntoFuture,
910    {
911        self.into_future()
912    }
913}
914
915impl<'a, C> MessageBuilder<'a, C, TextKind> {
916    pub fn with_image(self, img: Image) -> MessageBuilder<'a, C, PreviewableKind> {
917        let (msg, kind) = fuse_messages(self.msg, img);
918
919        MessageBuilder {
920            client: self.client,
921            chat_id: self.chat_id,
922            live: self.live,
923            ttl: self.ttl,
924            msg,
925            kind,
926        }
927    }
928
929    pub fn with_video(self, vid: Video) -> MessageBuilder<'a, C, PreviewableKind> {
930        let (msg, kind) = fuse_messages(self.msg, vid);
931
932        MessageBuilder {
933            client: self.client,
934            chat_id: self.chat_id,
935            live: self.live,
936            ttl: self.ttl,
937            msg,
938            kind,
939        }
940    }
941
942    pub fn with_link(self, link: Link) -> MessageBuilder<'a, C, PreviewableKind> {
943        let (msg, kind) = fuse_messages(self.msg, link);
944
945        MessageBuilder {
946            client: self.client,
947            chat_id: self.chat_id,
948            live: self.live,
949            ttl: self.ttl,
950            msg,
951            kind,
952        }
953    }
954
955    pub fn attach(self, img: CryptoFile) -> MessageBuilder<'a, C, RichKind> {
956        let (msg, kind) = fuse_messages(self.msg, img);
957
958        MessageBuilder {
959            client: self.client,
960            chat_id: self.chat_id,
961            live: self.live,
962            ttl: self.ttl,
963            msg,
964            kind,
965        }
966    }
967
968    pub fn report(self, reason: ReportReason) -> MessageBuilder<'a, C, RichKind> {
969        let (msg, kind) = fuse_messages(self.msg, reason);
970
971        MessageBuilder {
972            client: self.client,
973            chat_id: self.chat_id,
974            live: self.live,
975            ttl: self.ttl,
976            msg,
977            kind,
978        }
979    }
980
981    pub fn link_chat(self, chat: Chat) -> MessageBuilder<'a, C, RichKind> {
982        let (msg, kind) = fuse_messages(self.msg, chat);
983
984        MessageBuilder {
985            client: self.client,
986            chat_id: self.chat_id,
987            live: self.live,
988            ttl: self.ttl,
989            msg,
990            kind,
991        }
992    }
993}
994
995impl<'a, C> MessageBuilder<'a, C, RichKind> {
996    pub fn set_file_source(mut self, source: CryptoFile) -> Self {
997        self.msg.file_source = Some(source);
998        self
999    }
1000}
1001
1002impl<'a, C> MessageBuilder<'a, C, PreviewableKind> {
1003    /// Override the current image preview with a custom one
1004    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1005        self.kind.0 = preview;
1006        self
1007    }
1008
1009    #[cfg(feature = "multimedia")]
1010    /// Alter the default preview transcoder
1011    pub fn with_transcoder(mut self, transcoder: preview::Transcoder) -> Self {
1012        self.kind.0.set_transcoder(transcoder);
1013        self
1014    }
1015}
1016
1017mod sealed {
1018    pub trait SimplySendable {}
1019    impl SimplySendable for super::TextKind {}
1020    impl SimplySendable for super::RichKind {}
1021    impl SimplySendable for super::RawKind {}
1022}
1023
1024impl<'a, C, M> IntoFuture for MessageBuilder<'a, C, M>
1025where
1026    C: 'static + ClientApi,
1027    C::Error: 'static + Send,
1028    M: sealed::SimplySendable,
1029{
1030    type Output = Result<Arc<NewChatItemsResponse>, C::Error>;
1031    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1032
1033    fn into_future(self) -> Self::IntoFuture {
1034        Box::pin(self.client.api_send_messages(ApiSendMessages {
1035            send_ref: self.chat_id.into_chat_ref(),
1036            live_message: self.live,
1037            ttl: self.ttl.map(preferences::timed_messages::ttl_to_secs),
1038            composed_messages: vec![self.msg],
1039        }))
1040    }
1041}
1042
1043impl<'a, C> IntoFuture for MessageBuilder<'a, C, PreviewableKind>
1044where
1045    C: 'static + ClientApi,
1046    C::Error: 'static + Send,
1047{
1048    type Output = Result<Arc<NewChatItemsResponse>, C::Error>;
1049    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1050
1051    fn into_future(self) -> Self::IntoFuture {
1052        Box::pin(async move {
1053            let preview_data = self.kind.0.resolve().await;
1054            let mut msg = self.msg;
1055            msg.msg_content.set_preview(preview_data);
1056
1057            self.client
1058                .api_send_messages(ApiSendMessages {
1059                    send_ref: self.chat_id.into_chat_ref(),
1060                    live_message: self.live,
1061                    ttl: self.ttl.map(preferences::timed_messages::ttl_to_secs),
1062                    composed_messages: vec![msg],
1063                })
1064                .await
1065        })
1066    }
1067}
1068
1069pub struct MulticastBuilder<'a, I, C: 'a + ?Sized, M = TextKind> {
1070    pub(crate) client: &'a C,
1071    pub(crate) chat_ids: I,
1072    pub(crate) ttl: Option<Duration>,
1073    pub(crate) msg: ComposedMessage,
1074    pub(crate) kind: M,
1075}
1076
1077impl<'a, I, C, M> MulticastBuilder<'a, I, C, M> {
1078    pub fn with_ttl(mut self, ttl: Duration) -> Self {
1079        self.ttl = Some(ttl);
1080        self
1081    }
1082
1083    pub fn set_text(mut self, text: impl Into<String>) -> Self {
1084        self.msg.msg_content.set_text_part(text);
1085        self
1086    }
1087
1088    /// A syntactic sugar to avoid double awaits(`.await.await` -> `.await.send().await`) in
1089    /// certain use-cases
1090    pub fn send(self) -> <Self as IntoFuture>::IntoFuture
1091    where
1092        Self: IntoFuture,
1093    {
1094        self.into_future()
1095    }
1096}
1097
1098impl<'a, I, C> MulticastBuilder<'a, I, C, TextKind> {
1099    pub fn with_image(self, img: Image) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1100        let (msg, kind) = fuse_messages(self.msg, img);
1101
1102        MulticastBuilder {
1103            client: self.client,
1104            chat_ids: self.chat_ids,
1105            ttl: self.ttl,
1106            msg,
1107            kind,
1108        }
1109    }
1110
1111    pub fn with_video(self, vid: Video) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1112        let (msg, kind) = fuse_messages(self.msg, vid);
1113
1114        MulticastBuilder {
1115            client: self.client,
1116            chat_ids: self.chat_ids,
1117            ttl: self.ttl,
1118            msg,
1119            kind,
1120        }
1121    }
1122
1123    pub fn with_link(self, link: Link) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1124        let (msg, kind) = fuse_messages(self.msg, link);
1125
1126        MulticastBuilder {
1127            client: self.client,
1128            chat_ids: self.chat_ids,
1129            ttl: self.ttl,
1130            msg,
1131            kind,
1132        }
1133    }
1134
1135    pub fn attach(self, img: CryptoFile) -> MulticastBuilder<'a, I, C, RichKind> {
1136        let (msg, kind) = fuse_messages(self.msg, img);
1137
1138        MulticastBuilder {
1139            client: self.client,
1140            chat_ids: self.chat_ids,
1141            ttl: self.ttl,
1142            msg,
1143            kind,
1144        }
1145    }
1146
1147    pub fn report(self, reason: ReportReason) -> MulticastBuilder<'a, I, C, RichKind> {
1148        let (msg, kind) = fuse_messages(self.msg, reason);
1149
1150        MulticastBuilder {
1151            client: self.client,
1152            chat_ids: self.chat_ids,
1153            ttl: self.ttl,
1154            msg,
1155            kind,
1156        }
1157    }
1158
1159    pub fn link_chat(self, chat: Chat) -> MulticastBuilder<'a, I, C, RichKind> {
1160        let (msg, kind) = fuse_messages(self.msg, chat);
1161
1162        MulticastBuilder {
1163            client: self.client,
1164            chat_ids: self.chat_ids,
1165            ttl: self.ttl,
1166            msg,
1167            kind,
1168        }
1169    }
1170}
1171
1172impl<'a, I, C> MulticastBuilder<'a, I, C, RichKind> {
1173    pub fn set_file_source(mut self, source: CryptoFile) -> Self {
1174        self.msg.file_source = Some(source);
1175        self
1176    }
1177}
1178
1179impl<'a, I, C> MulticastBuilder<'a, I, C, PreviewableKind> {
1180    /// Override the current image preview with a custom one
1181    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1182        self.kind.0 = preview;
1183        self
1184    }
1185
1186    #[cfg(feature = "multimedia")]
1187    pub fn with_transcoder(mut self, transcoder: preview::Transcoder) -> Self {
1188        self.kind.0.set_transcoder(transcoder);
1189        self
1190    }
1191}
1192
1193impl<'a, I, C, M> IntoFuture for MulticastBuilder<'a, I, C, M>
1194where
1195    I: IntoIterator<Item = ChatId>,
1196    C: 'static + ClientApi,
1197    C::Error: 'static + Send,
1198    M: sealed::SimplySendable,
1199{
1200    type Output = Vec<Result<Arc<NewChatItemsResponse>, C::Error>>;
1201    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1202
1203    fn into_future(self) -> Self::IntoFuture {
1204        let Self {
1205            client,
1206            chat_ids,
1207            ttl,
1208            msg,
1209            kind: _,
1210        } = self;
1211
1212        let iter = chat_ids.into_iter().map(move |id| {
1213            let msg = msg.clone();
1214            async move {
1215                let command = ApiSendMessages {
1216                    send_ref: id.into_chat_ref(),
1217                    live_message: false,
1218                    ttl: ttl.map(preferences::timed_messages::ttl_to_secs),
1219                    composed_messages: vec![msg],
1220                };
1221
1222                client.api_send_messages(command).await
1223            }
1224        });
1225
1226        Box::pin(futures::future::join_all(iter))
1227    }
1228}
1229
1230impl<'a, I, C> IntoFuture for MulticastBuilder<'a, I, C, PreviewableKind>
1231where
1232    I: 'static + Send + IntoIterator<Item = ChatId>,
1233    C: 'static + ClientApi,
1234    C::Error: 'static + Send,
1235{
1236    type Output = Vec<Result<Arc<NewChatItemsResponse>, C::Error>>;
1237    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1238
1239    fn into_future(self) -> Self::IntoFuture {
1240        let Self {
1241            client,
1242            chat_ids,
1243            ttl,
1244            mut msg,
1245            kind,
1246        } = self;
1247
1248        Box::pin(async move {
1249            let preview_data = kind.0.resolve().await;
1250            msg.msg_content.set_preview(preview_data);
1251
1252            let iter = chat_ids.into_iter().map(move |id| {
1253                let msg = msg.clone();
1254                async move {
1255                    let command = ApiSendMessages {
1256                        send_ref: id.into_chat_ref(),
1257                        live_message: false,
1258                        ttl: ttl.map(preferences::timed_messages::ttl_to_secs),
1259                        composed_messages: vec![msg],
1260                    };
1261
1262                    client.api_send_messages(command).await
1263                }
1264            });
1265
1266            futures::future::join_all(iter).await
1267        })
1268    }
1269}
1270
1271fn fuse_messages<M: MessageLike>(old: ComposedMessage, new: M) -> (ComposedMessage, M::Kind) {
1272    let (mut new, kind) = new.into_builder_parts();
1273    new.quoted_item_id = old.quoted_item_id;
1274
1275    if new.msg_content.text_part().unwrap_or_default().is_empty() {
1276        new.msg_content.set_text_part(
1277            old.msg_content
1278                .text_part()
1279                .map(|s| s.to_owned())
1280                .unwrap_or_default(),
1281        );
1282    }
1283
1284    (new, kind)
1285}
1286
1287fn wrap_content(msg_content: MsgContent) -> ComposedMessage {
1288    ComposedMessage {
1289        file_source: None,
1290        quoted_item_id: None,
1291        msg_content,
1292        mentions: Default::default(),
1293        undocumented: Default::default(),
1294    }
1295}
1296
1297pub trait MsgContentExt {
1298    fn text_part(&self) -> Option<&str>;
1299
1300    fn text_part_mut(&mut self) -> Option<&mut String>;
1301
1302    fn set_text_part(&mut self, new_text: impl Into<String>) {
1303        if let Some(text) = self.text_part_mut() {
1304            *text = new_text.into();
1305        }
1306    }
1307
1308    fn preview(&self) -> Option<&str>;
1309
1310    fn preview_mut(&mut self) -> Option<&mut String>;
1311
1312    fn set_preview(&mut self, new_preview: String) {
1313        if let Some(preview) = self.preview_mut() {
1314            *preview = new_preview;
1315        }
1316    }
1317}
1318
1319impl MsgContentExt for MsgContent {
1320    fn text_part(&self) -> Option<&str> {
1321        match self {
1322            MsgContent::Text { text, .. }
1323            | MsgContent::Link { text, .. }
1324            | MsgContent::Image { text, .. }
1325            | MsgContent::Video { text, .. }
1326            | MsgContent::Voice { text, .. }
1327            | MsgContent::File { text, .. }
1328            | MsgContent::Report { text, .. }
1329            | MsgContent::Chat { text, .. }
1330            | MsgContent::Unknown { text, .. } => Some(text),
1331            _ => None,
1332        }
1333    }
1334
1335    fn text_part_mut(&mut self) -> Option<&mut String> {
1336        match self {
1337            MsgContent::Text { text, .. }
1338            | MsgContent::Link { text, .. }
1339            | MsgContent::Image { text, .. }
1340            | MsgContent::Video { text, .. }
1341            | MsgContent::Voice { text, .. }
1342            | MsgContent::File { text, .. }
1343            | MsgContent::Report { text, .. }
1344            | MsgContent::Chat { text, .. }
1345            | MsgContent::Unknown { text, .. } => Some(text),
1346            _ => None,
1347        }
1348    }
1349
1350    fn preview(&self) -> Option<&str> {
1351        match self {
1352            MsgContent::Link {
1353                preview: LinkPreview { image, .. },
1354                ..
1355            }
1356            | MsgContent::Image { image, .. }
1357            | MsgContent::Video { image, .. } => Some(image),
1358            _ => None,
1359        }
1360    }
1361
1362    fn preview_mut(&mut self) -> Option<&mut String> {
1363        match self {
1364            MsgContent::Link {
1365                preview: LinkPreview { image, .. },
1366                ..
1367            }
1368            | MsgContent::Image { image, .. }
1369            | MsgContent::Video { image, .. } => Some(image),
1370            _ => None,
1371        }
1372    }
1373}