1use 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
264pub struct TextKind;
266
267pub struct RichKind;
270
271pub struct RawKind;
274
275pub 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#[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
357pub 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#[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#[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#[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#[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#[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#[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#[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 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
874pub 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 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 pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1005 self.kind.0 = preview;
1006 self
1007 }
1008
1009 #[cfg(feature = "multimedia")]
1010 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 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 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}