deltachat/
chat.rs

1//! # Chat module.
2
3use std::cmp;
4use std::collections::{HashMap, HashSet};
5use std::fmt;
6use std::io::Cursor;
7use std::marker::Sync;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::time::Duration;
11
12use anyhow::{Context as _, Result, anyhow, bail, ensure};
13use chrono::TimeZone;
14use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
15use mail_builder::mime::MimePart;
16use serde::{Deserialize, Serialize};
17use strum_macros::EnumIter;
18
19use crate::blob::BlobObject;
20use crate::chatlist::Chatlist;
21use crate::color::str_to_color;
22use crate::config::Config;
23use crate::constants::{
24    Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
25    DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
26};
27use crate::contact::{self, Contact, ContactId, Origin};
28use crate::context::Context;
29use crate::debug_logging::maybe_set_logging_xdc;
30use crate::download::DownloadState;
31use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
32use crate::events::EventType;
33use crate::key::self_fingerprint;
34use crate::location;
35use crate::log::{LogExt, error, info, warn};
36use crate::logged_debug_assert;
37use crate::message::{self, Message, MessageState, MsgId, Viewtype};
38use crate::mimefactory::MimeFactory;
39use crate::mimeparser::SystemMessage;
40use crate::param::{Param, Params};
41use crate::receive_imf::ReceivedMsg;
42use crate::smtp::send_msg_to_smtp;
43use crate::stock_str;
44use crate::sync::{self, Sync::*, SyncData};
45use crate::tools::{
46    IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
47    create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
48    smeared_time, time, truncate_msg_text,
49};
50use crate::webxdc::StatusUpdateSerial;
51use crate::{chatlist_events, imap};
52
53/// An chat item, such as a message or a marker.
54#[derive(Debug, Copy, Clone, PartialEq, Eq)]
55pub enum ChatItem {
56    /// Chat message stored in the database.
57    Message {
58        /// Database ID of the message.
59        msg_id: MsgId,
60    },
61
62    /// Day marker, separating messages that correspond to different
63    /// days according to local time.
64    DayMarker {
65        /// Marker timestamp, for day markers
66        timestamp: i64,
67    },
68}
69
70/// The reason why messages cannot be sent to the chat.
71///
72/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub(crate) enum CantSendReason {
75    /// Special chat.
76    SpecialChat,
77
78    /// The chat is a device chat.
79    DeviceChat,
80
81    /// The chat is a contact request, it needs to be accepted before sending a message.
82    ContactRequest,
83
84    /// Mailing list without known List-Post header.
85    ReadOnlyMailingList,
86
87    /// Incoming broadcast channel where the user can't send messages.
88    InBroadcast,
89
90    /// Not a member of the chat.
91    NotAMember,
92
93    /// State for 1:1 chat with a key-contact that does not have a key.
94    MissingKey,
95}
96
97impl fmt::Display for CantSendReason {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::SpecialChat => write!(f, "the chat is a special chat"),
101            Self::DeviceChat => write!(f, "the chat is a device chat"),
102            Self::ContactRequest => write!(
103                f,
104                "contact request chat should be accepted before sending messages"
105            ),
106            Self::ReadOnlyMailingList => {
107                write!(f, "mailing list does not have a know post address")
108            }
109            Self::InBroadcast => {
110                write!(f, "Broadcast channel is read-only")
111            }
112            Self::NotAMember => write!(f, "not a member of the chat"),
113            Self::MissingKey => write!(f, "key is missing"),
114        }
115    }
116}
117
118/// Chat ID, including reserved IDs.
119///
120/// Some chat IDs are reserved to identify special chat types.  This
121/// type can represent both the special as well as normal chats.
122#[derive(
123    Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
124)]
125pub struct ChatId(u32);
126
127impl ChatId {
128    /// Create a new [ChatId].
129    pub const fn new(id: u32) -> ChatId {
130        ChatId(id)
131    }
132
133    /// An unset ChatId
134    ///
135    /// This is transitional and should not be used in new code.
136    pub fn is_unset(self) -> bool {
137        self.0 == 0
138    }
139
140    /// Whether the chat ID signifies a special chat.
141    ///
142    /// This kind of chat ID can not be used for real chats.
143    pub fn is_special(self) -> bool {
144        (0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0)
145    }
146
147    /// Chat ID for messages which need to be deleted.
148    ///
149    /// Messages which should be deleted get this chat ID and are
150    /// deleted later.  Deleted messages need to stay around as long
151    /// as they are not deleted on the server so that their rfc724_mid
152    /// remains known and downloading them again can be avoided.
153    pub fn is_trash(self) -> bool {
154        self == DC_CHAT_ID_TRASH
155    }
156
157    /// Chat ID signifying there are **any** number of archived chats.
158    ///
159    /// This chat ID can be returned in a [`Chatlist`] and signals to
160    /// the UI to include a link to the archived chats.
161    ///
162    /// [`Chatlist`]: crate::chatlist::Chatlist
163    pub fn is_archived_link(self) -> bool {
164        self == DC_CHAT_ID_ARCHIVED_LINK
165    }
166
167    /// Virtual chat ID signalling there are **only** archived chats.
168    ///
169    /// This can be included in the chatlist if the
170    /// [`DC_GCL_ADD_ALLDONE_HINT`] flag is used to build the
171    /// [`Chatlist`].
172    ///
173    /// [`DC_GCL_ADD_ALLDONE_HINT`]: crate::constants::DC_GCL_ADD_ALLDONE_HINT
174    /// [`Chatlist`]: crate::chatlist::Chatlist
175    pub fn is_alldone_hint(self) -> bool {
176        self == DC_CHAT_ID_ALLDONE_HINT
177    }
178
179    /// Returns [`ChatId`] of a chat that `msg` belongs to.
180    pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
181        if msg.chat_id == DC_CHAT_ID_TRASH {
182            return None;
183        }
184        if msg.download_state == DownloadState::Undecipherable {
185            return None;
186        }
187        Some(msg.chat_id)
188    }
189
190    /// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
191    /// if it exists and is not blocked.
192    ///
193    /// If the chat does not exist or is blocked, `None` is returned.
194    pub async fn lookup_by_contact(
195        context: &Context,
196        contact_id: ContactId,
197    ) -> Result<Option<Self>> {
198        let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, contact_id).await?
199        else {
200            return Ok(None);
201        };
202
203        let chat_id = match chat_id_blocked.blocked {
204            Blocked::Not | Blocked::Request => Some(chat_id_blocked.id),
205            Blocked::Yes => None,
206        };
207        Ok(chat_id)
208    }
209
210    /// Returns the [`ChatId`] for the 1:1 chat with `contact_id`.
211    ///
212    /// If the chat does not yet exist an unblocked chat ([`Blocked::Not`]) is created.
213    ///
214    /// This is an internal API, if **a user action** needs to get a chat
215    /// [`ChatId::create_for_contact`] should be used as this also scales up the
216    /// [`Contact`]'s origin.
217    pub(crate) async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
218        ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
219            .await
220            .map(|chat| chat.id)
221    }
222
223    /// Returns the unblocked 1:1 chat with `contact_id`.
224    ///
225    /// This should be used when **a user action** creates a chat 1:1, it ensures the chat
226    /// exists, is unblocked and scales the [`Contact`]'s origin.
227    pub async fn create_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
228        ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
229    }
230
231    /// Same as `create_for_contact()` with an additional `create_blocked` parameter
232    /// that is used in case the chat does not exist or to unblock existing chats.
233    /// `create_blocked` won't block already unblocked chats again.
234    pub(crate) async fn create_for_contact_with_blocked(
235        context: &Context,
236        contact_id: ContactId,
237        create_blocked: Blocked,
238    ) -> Result<Self> {
239        let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
240            Some(chat) => {
241                if create_blocked != Blocked::Not || chat.blocked == Blocked::Not {
242                    return Ok(chat.id);
243                }
244                chat.id.set_blocked(context, Blocked::Not).await?;
245                chat.id
246            }
247            None => {
248                if Contact::real_exists_by_id(context, contact_id).await?
249                    || contact_id == ContactId::SELF
250                {
251                    let chat_id =
252                        ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
253                            .await
254                            .map(|chat| chat.id)?;
255                    ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
256                    chat_id
257                } else {
258                    warn!(
259                        context,
260                        "Cannot create chat, contact {contact_id} does not exist."
261                    );
262                    bail!("Can not create chat for non-existing contact");
263                }
264            }
265        };
266        context.emit_msgs_changed_without_ids();
267        chatlist_events::emit_chatlist_changed(context);
268        chatlist_events::emit_chatlist_item_changed(context, chat_id);
269        Ok(chat_id)
270    }
271
272    /// Create a group or mailinglist raw database record with the given parameters.
273    /// The function does not add SELF nor checks if the record already exists.
274    pub(crate) async fn create_multiuser_record(
275        context: &Context,
276        chattype: Chattype,
277        grpid: &str,
278        grpname: &str,
279        create_blocked: Blocked,
280        param: Option<String>,
281        timestamp: i64,
282    ) -> Result<Self> {
283        let grpname = sanitize_single_line(grpname);
284        let timestamp = cmp::min(timestamp, smeared_time(context));
285        let row_id =
286            context.sql.insert(
287                "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, 0, ?);",
288                (
289                    chattype,
290                    &grpname,
291                    grpid,
292                    create_blocked,
293                    timestamp,
294                    param.unwrap_or_default(),
295                ),
296            ).await?;
297
298        let chat_id = ChatId::new(u32::try_from(row_id)?);
299        let chat = Chat::load_from_db(context, chat_id).await?;
300
301        if chat.is_encrypted(context).await? {
302            chat_id.add_encrypted_msg(context, timestamp).await?;
303        }
304
305        info!(
306            context,
307            "Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
308            &grpname,
309            grpid,
310            chat_id,
311            create_blocked,
312        );
313
314        Ok(chat_id)
315    }
316
317    async fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<()> {
318        context
319            .sql
320            .execute(
321                "UPDATE contacts
322                 SET selfavatar_sent=?
323                 WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=? AND add_timestamp >= remove_timestamp)",
324                (timestamp, self),
325            )
326            .await?;
327        Ok(())
328    }
329
330    /// Updates chat blocked status.
331    ///
332    /// Returns true if the value was modified.
333    pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
334        if self.is_special() {
335            bail!("ignoring setting of Block-status for {self}");
336        }
337        let count = context
338            .sql
339            .execute(
340                "UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
341                (new_blocked, self),
342            )
343            .await?;
344        Ok(count > 0)
345    }
346
347    /// Blocks the chat as a result of explicit user action.
348    pub async fn block(self, context: &Context) -> Result<()> {
349        self.block_ex(context, Sync).await
350    }
351
352    pub(crate) async fn block_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
353        let chat = Chat::load_from_db(context, self).await?;
354        let mut delete = false;
355
356        match chat.typ {
357            Chattype::OutBroadcast => {
358                bail!("Can't block chat of type {:?}", chat.typ)
359            }
360            Chattype::Single => {
361                for contact_id in get_chat_contacts(context, self).await? {
362                    if contact_id != ContactId::SELF {
363                        info!(
364                            context,
365                            "Blocking the contact {contact_id} to block 1:1 chat."
366                        );
367                        contact::set_blocked(context, Nosync, contact_id, true).await?;
368                    }
369                }
370            }
371            Chattype::Group => {
372                info!(context, "Can't block groups yet, deleting the chat.");
373                delete = true;
374            }
375            Chattype::Mailinglist | Chattype::InBroadcast => {
376                if self.set_blocked(context, Blocked::Yes).await? {
377                    context.emit_event(EventType::ChatModified(self));
378                }
379            }
380        }
381        chatlist_events::emit_chatlist_changed(context);
382
383        if sync.into() {
384            // NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
385            chat.sync(context, SyncAction::Block)
386                .await
387                .log_err(context)
388                .ok();
389        }
390        if delete {
391            self.delete_ex(context, Nosync).await?;
392        }
393        Ok(())
394    }
395
396    /// Unblocks the chat.
397    pub async fn unblock(self, context: &Context) -> Result<()> {
398        self.unblock_ex(context, Sync).await
399    }
400
401    pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
402        self.set_blocked(context, Blocked::Not).await?;
403
404        chatlist_events::emit_chatlist_changed(context);
405
406        if sync.into() {
407            let chat = Chat::load_from_db(context, self).await?;
408            // TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
409            // Maybe we should unblock the contact locally too, this would also resolve discrepancy
410            // with `block()` which also blocks the contact.
411            chat.sync(context, SyncAction::Unblock)
412                .await
413                .log_err(context)
414                .ok();
415        }
416
417        Ok(())
418    }
419
420    /// Accept the contact request.
421    ///
422    /// Unblocks the chat and scales up origin of contacts.
423    pub async fn accept(self, context: &Context) -> Result<()> {
424        self.accept_ex(context, Sync).await
425    }
426
427    pub(crate) async fn accept_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
428        let chat = Chat::load_from_db(context, self).await?;
429
430        match chat.typ {
431            Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
432                // User has "created a chat" with all these contacts.
433                //
434                // Previously accepting a chat literally created a chat because unaccepted chats
435                // went to "contact requests" list rather than normal chatlist.
436                for contact_id in get_chat_contacts(context, self).await? {
437                    if contact_id != ContactId::SELF {
438                        ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
439                            .await?;
440                    }
441                }
442            }
443            Chattype::Mailinglist => {
444                // If the message is from a mailing list, the contacts are not counted as "known"
445            }
446        }
447
448        if self.set_blocked(context, Blocked::Not).await? {
449            context.emit_event(EventType::ChatModified(self));
450            chatlist_events::emit_chatlist_item_changed(context, self);
451        }
452
453        if sync.into() {
454            chat.sync(context, SyncAction::Accept)
455                .await
456                .log_err(context)
457                .ok();
458        }
459        Ok(())
460    }
461
462    /// Adds message "Messages are end-to-end encrypted".
463    async fn add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
464        let text = stock_str::messages_e2e_encrypted(context).await;
465        add_info_msg_with_cmd(
466            context,
467            self,
468            &text,
469            SystemMessage::ChatE2ee,
470            timestamp_sort,
471            None,
472            None,
473            None,
474            None,
475        )
476        .await?;
477        Ok(())
478    }
479
480    /// Archives or unarchives a chat.
481    pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
482        self.set_visibility_ex(context, Sync, visibility).await
483    }
484
485    pub(crate) async fn set_visibility_ex(
486        self,
487        context: &Context,
488        sync: sync::Sync,
489        visibility: ChatVisibility,
490    ) -> Result<()> {
491        ensure!(
492            !self.is_special(),
493            "bad chat_id, can not be special chat: {self}"
494        );
495
496        context
497            .sql
498            .transaction(move |transaction| {
499                if visibility == ChatVisibility::Archived {
500                    transaction.execute(
501                        "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
502                        (MessageState::InNoticed, self, MessageState::InFresh),
503                    )?;
504                }
505                transaction.execute(
506                    "UPDATE chats SET archived=? WHERE id=?;",
507                    (visibility, self),
508                )?;
509                Ok(())
510            })
511            .await?;
512
513        if visibility == ChatVisibility::Archived {
514            start_chat_ephemeral_timers(context, self).await?;
515        }
516
517        context.emit_msgs_changed_without_ids();
518        chatlist_events::emit_chatlist_changed(context);
519        chatlist_events::emit_chatlist_item_changed(context, self);
520
521        if sync.into() {
522            let chat = Chat::load_from_db(context, self).await?;
523            chat.sync(context, SyncAction::SetVisibility(visibility))
524                .await
525                .log_err(context)
526                .ok();
527        }
528        Ok(())
529    }
530
531    /// Unarchives a chat that is archived and not muted.
532    /// Needed after a message is added to a chat so that the chat gets a normal visibility again.
533    /// `msg_state` is the state of the message. Matters only for incoming messages currently. For
534    /// multiple outgoing messages the function may be called once with MessageState::Undefined.
535    /// Sending an appropriate event is up to the caller.
536    /// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
537    /// chats with unread messages increases (which is possible if the chat is muted).
538    pub async fn unarchive_if_not_muted(
539        self,
540        context: &Context,
541        msg_state: MessageState,
542    ) -> Result<()> {
543        if msg_state != MessageState::InFresh {
544            context
545                .sql
546                .execute(
547                    "UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
548                AND NOT(muted_until=-1 OR muted_until>?)",
549                    (self, time()),
550                )
551                .await?;
552            return Ok(());
553        }
554        let chat = Chat::load_from_db(context, self).await?;
555        if chat.visibility != ChatVisibility::Archived {
556            return Ok(());
557        }
558        if chat.is_muted() {
559            let unread_cnt = context
560                .sql
561                .count(
562                    "SELECT COUNT(*)
563                FROM msgs
564                WHERE state=?
565                AND hidden=0
566                AND chat_id=?",
567                    (MessageState::InFresh, self),
568                )
569                .await?;
570            if unread_cnt == 1 {
571                // Added the first unread message in the chat.
572                context.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
573            }
574            return Ok(());
575        }
576        context
577            .sql
578            .execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
579            .await?;
580        Ok(())
581    }
582
583    /// Emits an appropriate event for a message. `important` is whether a notification should be
584    /// shown.
585    pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
586        if important {
587            debug_assert!(!msg_id.is_unset());
588
589            context.emit_incoming_msg(self, msg_id);
590        } else {
591            context.emit_msgs_changed(self, msg_id);
592        }
593    }
594
595    /// Deletes a chat.
596    pub async fn delete(self, context: &Context) -> Result<()> {
597        self.delete_ex(context, Sync).await
598    }
599
600    pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
601        ensure!(
602            !self.is_special(),
603            "bad chat_id, can not be a special chat: {self}"
604        );
605
606        let chat = Chat::load_from_db(context, self).await?;
607        let delete_msgs_target = context.get_delete_msgs_target().await?;
608        let sync_id = match sync {
609            Nosync => None,
610            Sync => chat.get_sync_id(context).await?,
611        };
612
613        context
614            .sql
615            .transaction(|transaction| {
616                transaction.execute(
617                    "UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
618                    (delete_msgs_target, self,),
619                )?;
620                transaction.execute(
621                    "DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
622                    (self,),
623                )?;
624                transaction.execute(
625                    "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
626                    (self,),
627                )?;
628                transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
629                transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
630                transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
631                Ok(())
632            })
633            .await?;
634
635        context.emit_event(EventType::ChatDeleted { chat_id: self });
636        context.emit_msgs_changed_without_ids();
637
638        if let Some(id) = sync_id {
639            self::sync(context, id, SyncAction::Delete)
640                .await
641                .log_err(context)
642                .ok();
643        }
644
645        if chat.is_self_talk() {
646            let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
647            add_device_msg(context, None, Some(&mut msg)).await?;
648        }
649        chatlist_events::emit_chatlist_changed(context);
650
651        context
652            .set_config_internal(Config::LastHousekeeping, None)
653            .await?;
654        context.scheduler.interrupt_inbox().await;
655
656        Ok(())
657    }
658
659    /// Sets draft message.
660    ///
661    /// Passing `None` as message just deletes the draft
662    pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
663        if self.is_special() {
664            return Ok(());
665        }
666
667        let changed = match &mut msg {
668            None => self.maybe_delete_draft(context).await?,
669            Some(msg) => self.do_set_draft(context, msg).await?,
670        };
671
672        if changed {
673            if msg.is_some() {
674                match self.get_draft_msg_id(context).await? {
675                    Some(msg_id) => context.emit_msgs_changed(self, msg_id),
676                    None => context.emit_msgs_changed_without_msg_id(self),
677                }
678            } else {
679                context.emit_msgs_changed_without_msg_id(self)
680            }
681        }
682
683        Ok(())
684    }
685
686    /// Returns ID of the draft message, if there is one.
687    async fn get_draft_msg_id(self, context: &Context) -> Result<Option<MsgId>> {
688        let msg_id: Option<MsgId> = context
689            .sql
690            .query_get_value(
691                "SELECT id FROM msgs WHERE chat_id=? AND state=?;",
692                (self, MessageState::OutDraft),
693            )
694            .await?;
695        Ok(msg_id)
696    }
697
698    /// Returns draft message, if there is one.
699    pub async fn get_draft(self, context: &Context) -> Result<Option<Message>> {
700        if self.is_special() {
701            return Ok(None);
702        }
703        match self.get_draft_msg_id(context).await? {
704            Some(draft_msg_id) => {
705                let msg = Message::load_from_db(context, draft_msg_id).await?;
706                Ok(Some(msg))
707            }
708            None => Ok(None),
709        }
710    }
711
712    /// Deletes draft message, if there is one.
713    ///
714    /// Returns `true`, if message was deleted, `false` otherwise.
715    async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
716        Ok(context
717            .sql
718            .execute(
719                "DELETE FROM msgs WHERE chat_id=? AND state=?",
720                (self, MessageState::OutDraft),
721            )
722            .await?
723            > 0)
724    }
725
726    /// Set provided message as draft message for specified chat.
727    /// Returns true if the draft was added or updated in place.
728    async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
729        match msg.viewtype {
730            Viewtype::Unknown => bail!("Can not set draft of unknown type."),
731            Viewtype::Text => {
732                if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() {
733                    bail!("No text and no quote in draft");
734                }
735            }
736            _ => {
737                if msg.viewtype == Viewtype::File {
738                    if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
739                        // We do not do an automatic conversion to other viewtypes here so that
740                        // users can send images as "files" to preserve the original quality
741                        // (usually we compress images). The remaining conversions are done by
742                        // `prepare_msg_blob()` later.
743                        .filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
744                    {
745                        msg.viewtype = better_type;
746                    }
747                }
748                if msg.viewtype == Viewtype::Vcard {
749                    let blob = msg
750                        .param
751                        .get_file_blob(context)?
752                        .context("no file stored in params")?;
753                    msg.try_set_vcard(context, &blob.to_abs_path()).await?;
754                }
755            }
756        }
757
758        // set back draft information to allow identifying the draft later on -
759        // no matter if message object is reused or reloaded from db
760        msg.state = MessageState::OutDraft;
761        msg.chat_id = self;
762
763        // if possible, replace existing draft and keep id
764        if !msg.id.is_special() {
765            if let Some(old_draft) = self.get_draft(context).await? {
766                if old_draft.id == msg.id
767                    && old_draft.chat_id == self
768                    && old_draft.state == MessageState::OutDraft
769                {
770                    let affected_rows = context
771                        .sql.execute(
772                                "UPDATE msgs
773                                SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
774                                WHERE id=?7
775                                AND (type <> ?2 
776                                    OR txt <> ?3 
777                                    OR txt_normalized <> ?4
778                                    OR param <> ?5
779                                    OR mime_in_reply_to <> ?6);",
780                                (
781                                    time(),
782                                    msg.viewtype,
783                                    &msg.text,
784                                    message::normalize_text(&msg.text),
785                                    msg.param.to_string(),
786                                    msg.in_reply_to.as_deref().unwrap_or_default(),
787                                    msg.id,
788                                ),
789                            ).await?;
790                    return Ok(affected_rows > 0);
791                }
792            }
793        }
794
795        let row_id = context
796            .sql
797            .transaction(|transaction| {
798                // Delete existing draft if it exists.
799                transaction.execute(
800                    "DELETE FROM msgs WHERE chat_id=? AND state=?",
801                    (self, MessageState::OutDraft),
802                )?;
803
804                // Insert new draft.
805                transaction.execute(
806                    "INSERT INTO msgs (
807                 chat_id,
808                 rfc724_mid,
809                 from_id,
810                 timestamp,
811                 type,
812                 state,
813                 txt,
814                 txt_normalized,
815                 param,
816                 hidden,
817                 mime_in_reply_to)
818         VALUES (?,?,?,?,?,?,?,?,?,?,?);",
819                    (
820                        self,
821                        &msg.rfc724_mid,
822                        ContactId::SELF,
823                        time(),
824                        msg.viewtype,
825                        MessageState::OutDraft,
826                        &msg.text,
827                        message::normalize_text(&msg.text),
828                        msg.param.to_string(),
829                        1,
830                        msg.in_reply_to.as_deref().unwrap_or_default(),
831                    ),
832                )?;
833
834                Ok(transaction.last_insert_rowid())
835            })
836            .await?;
837        msg.id = MsgId::new(row_id.try_into()?);
838        Ok(true)
839    }
840
841    /// Returns number of messages in a chat.
842    pub async fn get_msg_cnt(self, context: &Context) -> Result<usize> {
843        let count = context
844            .sql
845            .count(
846                "SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
847                (self,),
848            )
849            .await?;
850        Ok(count)
851    }
852
853    /// Returns the number of fresh messages in the chat.
854    pub async fn get_fresh_msg_cnt(self, context: &Context) -> Result<usize> {
855        // this function is typically used to show a badge counter beside _each_ chatlist item.
856        // to make this as fast as possible, esp. on older devices, we added an combined index over the rows used for querying.
857        // so if you alter the query here, you may want to alter the index over `(state, hidden, chat_id)` in `sql.rs`.
858        //
859        // the impact of the index is significant once the database grows:
860        // - on an older android4 with 18k messages, query-time decreased from 110ms to 2ms
861        // - on an mid-class moto-g or iphone7 with 50k messages, query-time decreased from 26ms or 6ms to 0-1ms
862        // the times are average, no matter if there are fresh messages or not -
863        // and have to be multiplied by the number of items shown at once on the chatlist,
864        // so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
865        let count = if self.is_archived_link() {
866            context
867                .sql
868                .count(
869                    "SELECT COUNT(DISTINCT(m.chat_id))
870                    FROM msgs m
871                    LEFT JOIN chats c ON m.chat_id=c.id
872                    WHERE m.state=10
873                    and m.hidden=0
874                    AND m.chat_id>9
875                    AND c.blocked=0
876                    AND c.archived=1
877                    ",
878                    (),
879                )
880                .await?
881        } else {
882            context
883                .sql
884                .count(
885                    "SELECT COUNT(*)
886                FROM msgs
887                WHERE state=?
888                AND hidden=0
889                AND chat_id=?;",
890                    (MessageState::InFresh, self),
891                )
892                .await?
893        };
894        Ok(count)
895    }
896
897    pub(crate) async fn created_timestamp(self, context: &Context) -> Result<i64> {
898        Ok(context
899            .sql
900            .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,))
901            .await?
902            .unwrap_or(0))
903    }
904
905    /// Returns timestamp of the latest message in the chat,
906    /// including hidden messages or a draft if there is one.
907    pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
908        let timestamp = context
909            .sql
910            .query_get_value(
911                "SELECT MAX(timestamp)
912                 FROM msgs
913                 WHERE chat_id=?
914                 HAVING COUNT(*) > 0",
915                (self,),
916            )
917            .await?;
918        Ok(timestamp)
919    }
920
921    /// Returns a list of active similar chat IDs sorted by similarity metric.
922    ///
923    /// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
924    ///
925    /// Chat is considered active if something was posted there within the last 42 days.
926    pub async fn get_similar_chat_ids(self, context: &Context) -> Result<Vec<(ChatId, f64)>> {
927        // Count number of common members in this and other chats.
928        let intersection = context
929            .sql
930            .query_map_vec(
931                "SELECT y.chat_id, SUM(x.contact_id = y.contact_id)
932                 FROM chats_contacts as x
933                 JOIN chats_contacts as y
934                 WHERE x.contact_id > 9
935                   AND y.contact_id > 9
936                   AND x.add_timestamp >= x.remove_timestamp
937                   AND y.add_timestamp >= y.remove_timestamp
938                   AND x.chat_id=?
939                   AND y.chat_id<>x.chat_id
940                   AND y.chat_id>?
941                 GROUP BY y.chat_id",
942                (self, DC_CHAT_ID_LAST_SPECIAL),
943                |row| {
944                    let chat_id: ChatId = row.get(0)?;
945                    let intersection: f64 = row.get(1)?;
946                    Ok((chat_id, intersection))
947                },
948            )
949            .await
950            .context("failed to calculate member set intersections")?;
951
952        let chat_size: HashMap<ChatId, f64> = context
953            .sql
954            .query_map_collect(
955                "SELECT chat_id, count(*) AS n
956                 FROM chats_contacts
957                 WHERE contact_id > ? AND chat_id > ?
958                 AND add_timestamp >= remove_timestamp
959                 GROUP BY chat_id",
960                (ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
961                |row| {
962                    let chat_id: ChatId = row.get(0)?;
963                    let size: f64 = row.get(1)?;
964                    Ok((chat_id, size))
965                },
966            )
967            .await
968            .context("failed to count chat member sizes")?;
969
970        let our_chat_size = chat_size.get(&self).copied().unwrap_or_default();
971        let mut chats_with_metrics = Vec::new();
972        for (chat_id, intersection_size) in intersection {
973            if intersection_size > 0.0 {
974                let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default();
975                let union_size = our_chat_size + other_chat_size - intersection_size;
976                let metric = intersection_size / union_size;
977                chats_with_metrics.push((chat_id, metric))
978            }
979        }
980        chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| {
981            metric2
982                .partial_cmp(metric1)
983                .unwrap_or(chat_id2.cmp(chat_id1))
984        });
985
986        // Select up to five similar active chats.
987        let mut res = Vec::new();
988        let now = time();
989        for (chat_id, metric) in chats_with_metrics {
990            if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
991                if now > chat_timestamp + 42 * 24 * 3600 {
992                    // Chat was inactive for 42 days, skip.
993                    continue;
994                }
995            }
996
997            if metric < 0.1 {
998                // Chat is unrelated.
999                break;
1000            }
1001
1002            let chat = Chat::load_from_db(context, chat_id).await?;
1003            if chat.typ != Chattype::Group {
1004                continue;
1005            }
1006
1007            match chat.visibility {
1008                ChatVisibility::Normal | ChatVisibility::Pinned => {}
1009                ChatVisibility::Archived => continue,
1010            }
1011
1012            res.push((chat_id, metric));
1013            if res.len() >= 5 {
1014                break;
1015            }
1016        }
1017
1018        Ok(res)
1019    }
1020
1021    /// Returns similar chats as a [`Chatlist`].
1022    ///
1023    /// [`Chatlist`]: crate::chatlist::Chatlist
1024    pub async fn get_similar_chatlist(self, context: &Context) -> Result<Chatlist> {
1025        let chat_ids: Vec<ChatId> = self
1026            .get_similar_chat_ids(context)
1027            .await
1028            .context("failed to get similar chat IDs")?
1029            .into_iter()
1030            .map(|(chat_id, _metric)| chat_id)
1031            .collect();
1032        let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?;
1033        Ok(chatlist)
1034    }
1035
1036    pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
1037        let res: Option<String> = context
1038            .sql
1039            .query_get_value("SELECT param FROM chats WHERE id=?", (self,))
1040            .await?;
1041        Ok(res
1042            .map(|s| s.parse().unwrap_or_default())
1043            .unwrap_or_default())
1044    }
1045
1046    /// Returns true if the chat is not promoted.
1047    pub(crate) async fn is_unpromoted(self, context: &Context) -> Result<bool> {
1048        let param = self.get_param(context).await?;
1049        let unpromoted = param.get_bool(Param::Unpromoted).unwrap_or_default();
1050        Ok(unpromoted)
1051    }
1052
1053    /// Returns true if the chat is promoted.
1054    pub(crate) async fn is_promoted(self, context: &Context) -> Result<bool> {
1055        let promoted = !self.is_unpromoted(context).await?;
1056        Ok(promoted)
1057    }
1058
1059    /// Returns true if chat is a saved messages chat.
1060    pub async fn is_self_talk(self, context: &Context) -> Result<bool> {
1061        Ok(self.get_param(context).await?.exists(Param::Selftalk))
1062    }
1063
1064    /// Returns true if chat is a device chat.
1065    pub async fn is_device_talk(self, context: &Context) -> Result<bool> {
1066        Ok(self.get_param(context).await?.exists(Param::Devicetalk))
1067    }
1068
1069    async fn parent_query<T, F>(
1070        self,
1071        context: &Context,
1072        fields: &str,
1073        state_out_min: MessageState,
1074        f: F,
1075    ) -> Result<Option<T>>
1076    where
1077        F: Send + FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
1078        T: Send + 'static,
1079    {
1080        let sql = &context.sql;
1081        let query = format!(
1082            "SELECT {fields} \
1083             FROM msgs \
1084             WHERE chat_id=? \
1085             AND ((state BETWEEN {} AND {}) OR (state >= {})) \
1086             AND NOT hidden \
1087             AND download_state={} \
1088             AND from_id != {} \
1089             ORDER BY timestamp DESC, id DESC \
1090             LIMIT 1;",
1091            MessageState::InFresh as u32,
1092            MessageState::InSeen as u32,
1093            state_out_min as u32,
1094            // Do not reply to not fully downloaded messages. Such a message could be a group chat
1095            // message that we assigned to 1:1 chat.
1096            DownloadState::Done as u32,
1097            // Do not reference info messages, they are not actually sent out
1098            // and have Message-IDs unknown to other chat members.
1099            ContactId::INFO.to_u32(),
1100        );
1101        sql.query_row_optional(&query, (self,), f).await
1102    }
1103
1104    async fn get_parent_mime_headers(
1105        self,
1106        context: &Context,
1107        state_out_min: MessageState,
1108    ) -> Result<Option<(String, String, String)>> {
1109        self.parent_query(
1110            context,
1111            "rfc724_mid, mime_in_reply_to, IFNULL(mime_references, '')",
1112            state_out_min,
1113            |row: &rusqlite::Row| {
1114                let rfc724_mid: String = row.get(0)?;
1115                let mime_in_reply_to: String = row.get(1)?;
1116                let mime_references: String = row.get(2)?;
1117                Ok((rfc724_mid, mime_in_reply_to, mime_references))
1118            },
1119        )
1120        .await
1121    }
1122
1123    /// Returns multi-line text summary of encryption preferences of all chat contacts.
1124    ///
1125    /// This can be used to find out if encryption is not available because
1126    /// keys for some users are missing or simply because the majority of the users in a group
1127    /// prefer plaintext emails.
1128    ///
1129    /// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
1130    pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
1131        let chat = Chat::load_from_db(context, self).await?;
1132        if !chat.is_encrypted(context).await? {
1133            return Ok(stock_str::encr_none(context).await);
1134        }
1135
1136        let mut ret = stock_str::e2e_available(context).await + "\n";
1137
1138        for &contact_id in get_chat_contacts(context, self)
1139            .await?
1140            .iter()
1141            .filter(|&contact_id| !contact_id.is_special())
1142        {
1143            let contact = Contact::get_by_id(context, contact_id).await?;
1144            let addr = contact.get_addr();
1145            logged_debug_assert!(
1146                context,
1147                contact.is_key_contact(),
1148                "get_encryption_info: contact {contact_id} is not a key-contact."
1149            );
1150            let fingerprint = contact
1151                .fingerprint()
1152                .context("Contact does not have a fingerprint in encrypted chat")?;
1153            if contact.public_key(context).await?.is_some() {
1154                ret += &format!("\n{addr}\n{fingerprint}\n");
1155            } else {
1156                ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
1157            }
1158        }
1159
1160        Ok(ret.trim().to_string())
1161    }
1162
1163    /// Bad evil escape hatch.
1164    ///
1165    /// Avoid using this, eventually types should be cleaned up enough
1166    /// that it is no longer necessary.
1167    pub fn to_u32(self) -> u32 {
1168        self.0
1169    }
1170
1171    pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
1172        context
1173            .sql
1174            .execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
1175            .await?;
1176        Ok(())
1177    }
1178
1179    /// Returns the sort timestamp for a new message in the chat.
1180    ///
1181    /// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
1182    /// corresponding event in case of a system message (usually the current system time).
1183    /// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
1184    /// to the chat bottom.
1185    /// `received` -- whether the message is received. Otherwise being sent.
1186    /// `incoming` -- whether the message is incoming.
1187    pub(crate) async fn calc_sort_timestamp(
1188        self,
1189        context: &Context,
1190        message_timestamp: i64,
1191        always_sort_to_bottom: bool,
1192        received: bool,
1193        incoming: bool,
1194    ) -> Result<i64> {
1195        let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
1196
1197        let last_msg_time: Option<i64> = if always_sort_to_bottom {
1198            // get newest message for this chat
1199
1200            // Let hidden messages also be ordered with protection messages because hidden messages
1201            // also can be or not be verified, so let's preserve this information -- even it's not
1202            // used currently, it can be useful in the future versions.
1203            context
1204                .sql
1205                .query_get_value(
1206                    "SELECT MAX(timestamp)
1207                     FROM msgs
1208                     WHERE chat_id=? AND state!=?
1209                     HAVING COUNT(*) > 0",
1210                    (self, MessageState::OutDraft),
1211                )
1212                .await?
1213        } else if received {
1214            // Received messages shouldn't mingle with just sent ones and appear somewhere in the
1215            // middle of the chat, so we go after the newest non fresh message.
1216            //
1217            // But if a received outgoing message is older than some seen message, better sort the
1218            // received message purely by timestamp. We could place it just before that seen
1219            // message, but anyway the user may not notice it.
1220            //
1221            // NB: Received outgoing messages may break sorting of fresh incoming ones, but this
1222            // shouldn't happen frequently. Seen incoming messages don't really break sorting of
1223            // fresh ones, they rather mean that older incoming messages are actually seen as well.
1224            context
1225                .sql
1226                .query_row_optional(
1227                    "SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
1228                     FROM msgs
1229                     WHERE chat_id=? AND hidden=0 AND state>?
1230                     HAVING COUNT(*) > 0",
1231                    (MessageState::InSeen, self, MessageState::InFresh),
1232                    |row| {
1233                        let ts: i64 = row.get(0)?;
1234                        let ts_sent_seen: i64 = row.get(1)?;
1235                        Ok((ts, ts_sent_seen))
1236                    },
1237                )
1238                .await?
1239                .and_then(|(ts, ts_sent_seen)| {
1240                    match incoming || ts_sent_seen <= message_timestamp {
1241                        true => Some(ts),
1242                        false => None,
1243                    }
1244                })
1245        } else {
1246            None
1247        };
1248
1249        if let Some(last_msg_time) = last_msg_time {
1250            if last_msg_time > sort_timestamp {
1251                sort_timestamp = last_msg_time;
1252            }
1253        }
1254
1255        Ok(sort_timestamp)
1256    }
1257}
1258
1259impl std::fmt::Display for ChatId {
1260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1261        if self.is_trash() {
1262            write!(f, "Chat#Trash")
1263        } else if self.is_archived_link() {
1264            write!(f, "Chat#ArchivedLink")
1265        } else if self.is_alldone_hint() {
1266            write!(f, "Chat#AlldoneHint")
1267        } else if self.is_special() {
1268            write!(f, "Chat#Special{}", self.0)
1269        } else {
1270            write!(f, "Chat#{}", self.0)
1271        }
1272    }
1273}
1274
1275/// Allow converting [ChatId] to an SQLite type.
1276///
1277/// This allows you to directly store [ChatId] into the database as
1278/// well as query for a [ChatId].
1279impl rusqlite::types::ToSql for ChatId {
1280    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
1281        let val = rusqlite::types::Value::Integer(i64::from(self.0));
1282        let out = rusqlite::types::ToSqlOutput::Owned(val);
1283        Ok(out)
1284    }
1285}
1286
1287/// Allow converting an SQLite integer directly into [ChatId].
1288impl rusqlite::types::FromSql for ChatId {
1289    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
1290        i64::column_result(value).and_then(|val| {
1291            if 0 <= val && val <= i64::from(u32::MAX) {
1292                Ok(ChatId::new(val as u32))
1293            } else {
1294                Err(rusqlite::types::FromSqlError::OutOfRange(val))
1295            }
1296        })
1297    }
1298}
1299
1300/// An object representing a single chat in memory.
1301/// Chat objects are created using eg. `Chat::load_from_db`
1302/// and are not updated on database changes;
1303/// if you want an update, you have to recreate the object.
1304#[derive(Debug, Clone, Deserialize, Serialize)]
1305pub struct Chat {
1306    /// Database ID.
1307    pub id: ChatId,
1308
1309    /// Chat type, e.g. 1:1 chat, group chat, mailing list.
1310    pub typ: Chattype,
1311
1312    /// Chat name.
1313    pub name: String,
1314
1315    /// Whether the chat is archived or pinned.
1316    pub visibility: ChatVisibility,
1317
1318    /// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
1319    /// ad-hoc groups.
1320    pub grpid: String,
1321
1322    /// Whether the chat is blocked, unblocked or a contact request.
1323    pub blocked: Blocked,
1324
1325    /// Additional chat parameters stored in the database.
1326    pub param: Params,
1327
1328    /// If location streaming is enabled in the chat.
1329    is_sending_locations: bool,
1330
1331    /// Duration of the chat being muted.
1332    pub mute_duration: MuteDuration,
1333}
1334
1335impl Chat {
1336    /// Loads chat from the database by its ID.
1337    pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self> {
1338        let mut chat = context
1339            .sql
1340            .query_row(
1341                "SELECT c.type, c.name, c.grpid, c.param, c.archived,
1342                    c.blocked, c.locations_send_until, c.muted_until
1343             FROM chats c
1344             WHERE c.id=?;",
1345                (chat_id,),
1346                |row| {
1347                    let c = Chat {
1348                        id: chat_id,
1349                        typ: row.get(0)?,
1350                        name: row.get::<_, String>(1)?,
1351                        grpid: row.get::<_, String>(2)?,
1352                        param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
1353                        visibility: row.get(4)?,
1354                        blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
1355                        is_sending_locations: row.get(6)?,
1356                        mute_duration: row.get(7)?,
1357                    };
1358                    Ok(c)
1359                },
1360            )
1361            .await
1362            .context(format!("Failed loading chat {chat_id} from database"))?;
1363
1364        if chat.id.is_archived_link() {
1365            chat.name = stock_str::archived_chats(context).await;
1366        } else {
1367            if chat.typ == Chattype::Single && chat.name.is_empty() {
1368                // chat.name is set to contact.display_name on changes,
1369                // however, if things went wrong somehow, we do this here explicitly.
1370                let mut chat_name = "Err [Name not found]".to_owned();
1371                match get_chat_contacts(context, chat.id).await {
1372                    Ok(contacts) => {
1373                        if let Some(contact_id) = contacts.first() {
1374                            if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
1375                                contact.get_display_name().clone_into(&mut chat_name);
1376                            }
1377                        }
1378                    }
1379                    Err(err) => {
1380                        error!(
1381                            context,
1382                            "Failed to load contacts for {}: {:#}.", chat.id, err
1383                        );
1384                    }
1385                }
1386                chat.name = chat_name;
1387            }
1388            if chat.param.exists(Param::Selftalk) {
1389                chat.name = stock_str::saved_messages(context).await;
1390            } else if chat.param.exists(Param::Devicetalk) {
1391                chat.name = stock_str::device_messages(context).await;
1392            }
1393        }
1394
1395        Ok(chat)
1396    }
1397
1398    /// Returns whether this is the `saved messages` chat
1399    pub fn is_self_talk(&self) -> bool {
1400        self.param.exists(Param::Selftalk)
1401    }
1402
1403    /// Returns true if chat is a device chat.
1404    pub fn is_device_talk(&self) -> bool {
1405        self.param.exists(Param::Devicetalk)
1406    }
1407
1408    /// Returns true if chat is a mailing list.
1409    pub fn is_mailing_list(&self) -> bool {
1410        self.typ == Chattype::Mailinglist
1411    }
1412
1413    /// Returns None if user can send messages to this chat.
1414    ///
1415    /// Otherwise returns a reason useful for logging.
1416    pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
1417        self.why_cant_send_ex(context, &|_| false).await
1418    }
1419
1420    pub(crate) async fn why_cant_send_ex(
1421        &self,
1422        context: &Context,
1423        skip_fn: &(dyn Send + Sync + Fn(&CantSendReason) -> bool),
1424    ) -> Result<Option<CantSendReason>> {
1425        use CantSendReason::*;
1426        // NB: Don't forget to update Chatlist::try_load() when changing this function!
1427
1428        if self.id.is_special() {
1429            let reason = SpecialChat;
1430            if !skip_fn(&reason) {
1431                return Ok(Some(reason));
1432            }
1433        }
1434        if self.is_device_talk() {
1435            let reason = DeviceChat;
1436            if !skip_fn(&reason) {
1437                return Ok(Some(reason));
1438            }
1439        }
1440        if self.is_contact_request() {
1441            let reason = ContactRequest;
1442            if !skip_fn(&reason) {
1443                return Ok(Some(reason));
1444            }
1445        }
1446        if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
1447            let reason = ReadOnlyMailingList;
1448            if !skip_fn(&reason) {
1449                return Ok(Some(reason));
1450            }
1451        }
1452        if self.typ == Chattype::InBroadcast {
1453            let reason = InBroadcast;
1454            if !skip_fn(&reason) {
1455                return Ok(Some(reason));
1456            }
1457        }
1458
1459        // Do potentially slow checks last and after calls to `skip_fn` which should be fast.
1460        let reason = NotAMember;
1461        if !skip_fn(&reason) && !self.is_self_in_chat(context).await? {
1462            return Ok(Some(reason));
1463        }
1464
1465        let reason = MissingKey;
1466        if !skip_fn(&reason) && self.typ == Chattype::Single {
1467            let contact_ids = get_chat_contacts(context, self.id).await?;
1468            if let Some(contact_id) = contact_ids.first() {
1469                let contact = Contact::get_by_id(context, *contact_id).await?;
1470                if contact.is_key_contact() && contact.public_key(context).await?.is_none() {
1471                    return Ok(Some(reason));
1472                }
1473            }
1474        }
1475
1476        Ok(None)
1477    }
1478
1479    /// Returns true if can send to the chat.
1480    ///
1481    /// This function can be used by the UI to decide whether to display the input box.
1482    pub async fn can_send(&self, context: &Context) -> Result<bool> {
1483        Ok(self.why_cant_send(context).await?.is_none())
1484    }
1485
1486    /// Checks if the user is part of a chat
1487    /// and has basically the permissions to edit the chat therefore.
1488    /// The function does not check if the chat type allows editing of concrete elements.
1489    pub async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
1490        match self.typ {
1491            Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
1492            Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
1493            Chattype::InBroadcast => Ok(false),
1494        }
1495    }
1496
1497    pub(crate) async fn update_param(&mut self, context: &Context) -> Result<()> {
1498        context
1499            .sql
1500            .execute(
1501                "UPDATE chats SET param=? WHERE id=?",
1502                (self.param.to_string(), self.id),
1503            )
1504            .await?;
1505        Ok(())
1506    }
1507
1508    /// Returns chat ID.
1509    pub fn get_id(&self) -> ChatId {
1510        self.id
1511    }
1512
1513    /// Returns chat type.
1514    pub fn get_type(&self) -> Chattype {
1515        self.typ
1516    }
1517
1518    /// Returns chat name.
1519    pub fn get_name(&self) -> &str {
1520        &self.name
1521    }
1522
1523    /// Returns mailing list address where messages are sent to.
1524    pub fn get_mailinglist_addr(&self) -> Option<&str> {
1525        self.param.get(Param::ListPost)
1526    }
1527
1528    /// Returns profile image path for the chat.
1529    pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
1530        if self.id.is_archived_link() {
1531            // This is not a real chat, but the "Archive" button
1532            // that is shown at the top of the chats list
1533            return Ok(Some(get_archive_icon(context).await?));
1534        } else if self.is_device_talk() {
1535            return Ok(Some(get_device_icon(context).await?));
1536        } else if self.is_self_talk() {
1537            return Ok(Some(get_saved_messages_icon(context).await?));
1538        } else if !self.is_encrypted(context).await? {
1539            // This is an unencrypted chat, show a special avatar that marks it as such.
1540            return Ok(Some(get_abs_path(
1541                context,
1542                Path::new(&get_unencrypted_icon(context).await?),
1543            )));
1544        } else if self.typ == Chattype::Single {
1545            // For 1:1 chats, we always use the same avatar as for the contact
1546            // This is before the `self.is_encrypted()` check, because that function
1547            // has two database calls, i.e. it's slow
1548            let contacts = get_chat_contacts(context, self.id).await?;
1549            if let Some(contact_id) = contacts.first() {
1550                let contact = Contact::get_by_id(context, *contact_id).await?;
1551                return contact.get_profile_image(context).await;
1552            }
1553        } else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
1554            // Load the group avatar, or the device-chat / saved-messages icon
1555            if !image_rel.is_empty() {
1556                return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
1557            }
1558        }
1559        Ok(None)
1560    }
1561
1562    /// Returns chat avatar color.
1563    ///
1564    /// For 1:1 chats, the color is calculated from the contact's address
1565    /// for address-contacts and from the OpenPGP key fingerprint for key-contacts.
1566    /// For group chats the color is calculated from the grpid, if present, or the chat name.
1567    pub async fn get_color(&self, context: &Context) -> Result<u32> {
1568        let mut color = 0;
1569
1570        if self.typ == Chattype::Single {
1571            let contacts = get_chat_contacts(context, self.id).await?;
1572            if let Some(contact_id) = contacts.first() {
1573                if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
1574                    color = contact.get_color();
1575                }
1576            }
1577        } else if !self.grpid.is_empty() {
1578            color = str_to_color(&self.grpid);
1579        } else {
1580            color = str_to_color(&self.name);
1581        }
1582
1583        Ok(color)
1584    }
1585
1586    /// Returns a struct describing the current state of the chat.
1587    ///
1588    /// This is somewhat experimental, even more so than the rest of
1589    /// deltachat, and the data returned is still subject to change.
1590    pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
1591        let draft = match self.id.get_draft(context).await? {
1592            Some(message) => message.text,
1593            _ => String::new(),
1594        };
1595        Ok(ChatInfo {
1596            id: self.id,
1597            type_: self.typ as u32,
1598            name: self.name.clone(),
1599            archived: self.visibility == ChatVisibility::Archived,
1600            param: self.param.to_string(),
1601            is_sending_locations: self.is_sending_locations,
1602            color: self.get_color(context).await?,
1603            profile_image: self
1604                .get_profile_image(context)
1605                .await?
1606                .unwrap_or_else(std::path::PathBuf::new),
1607            draft,
1608            is_muted: self.is_muted(),
1609            ephemeral_timer: self.id.get_ephemeral_timer(context).await?,
1610        })
1611    }
1612
1613    /// Returns chat visibilitiy, e.g. whether it is archived or pinned.
1614    pub fn get_visibility(&self) -> ChatVisibility {
1615        self.visibility
1616    }
1617
1618    /// Returns true if chat is a contact request.
1619    ///
1620    /// Messages cannot be sent to such chat and read receipts are not
1621    /// sent until the chat is manually unblocked.
1622    pub fn is_contact_request(&self) -> bool {
1623        self.blocked == Blocked::Request
1624    }
1625
1626    /// Returns true if the chat is not promoted.
1627    pub fn is_unpromoted(&self) -> bool {
1628        self.param.get_bool(Param::Unpromoted).unwrap_or_default()
1629    }
1630
1631    /// Returns true if the chat is promoted.
1632    /// This means a message has been sent to it and it _not_ only exists on the users device.
1633    pub fn is_promoted(&self) -> bool {
1634        !self.is_unpromoted()
1635    }
1636
1637    /// Returns true if the chat is encrypted.
1638    pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
1639        let is_encrypted = match self.typ {
1640            Chattype::Single => {
1641                match context
1642                    .sql
1643                    .query_row_optional(
1644                        "SELECT cc.contact_id, c.fingerprint<>''
1645                             FROM chats_contacts cc LEFT JOIN contacts c
1646                                 ON c.id=cc.contact_id
1647                             WHERE cc.chat_id=?
1648                            ",
1649                        (self.id,),
1650                        |row| {
1651                            let id: ContactId = row.get(0)?;
1652                            let is_key: bool = row.get(1)?;
1653                            Ok((id, is_key))
1654                        },
1655                    )
1656                    .await?
1657                {
1658                    Some((id, is_key)) => is_key || id == ContactId::DEVICE,
1659                    None => true,
1660                }
1661            }
1662            Chattype::Group => {
1663                // Do not encrypt ad-hoc groups.
1664                !self.grpid.is_empty()
1665            }
1666            Chattype::Mailinglist => false,
1667            Chattype::OutBroadcast | Chattype::InBroadcast => true,
1668        };
1669        Ok(is_encrypted)
1670    }
1671
1672    /// Returns true if location streaming is enabled in the chat.
1673    pub fn is_sending_locations(&self) -> bool {
1674        self.is_sending_locations
1675    }
1676
1677    /// Returns true if the chat is currently muted.
1678    pub fn is_muted(&self) -> bool {
1679        match self.mute_duration {
1680            MuteDuration::NotMuted => false,
1681            MuteDuration::Forever => true,
1682            MuteDuration::Until(when) => when > SystemTime::now(),
1683        }
1684    }
1685
1686    /// Returns chat member list timestamp.
1687    pub(crate) async fn member_list_timestamp(&self, context: &Context) -> Result<i64> {
1688        if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
1689            Ok(member_list_timestamp)
1690        } else {
1691            Ok(self.id.created_timestamp(context).await?)
1692        }
1693    }
1694
1695    /// Returns true if member list is stale,
1696    /// i.e. has not been updated for 60 days.
1697    ///
1698    /// This is used primarily to detect the case
1699    /// where the user just restored an old backup.
1700    pub(crate) async fn member_list_is_stale(&self, context: &Context) -> Result<bool> {
1701        let now = time();
1702        let member_list_ts = self.member_list_timestamp(context).await?;
1703        let is_stale = now.saturating_add(TIMESTAMP_SENT_TOLERANCE)
1704            >= member_list_ts.saturating_add(60 * 24 * 3600);
1705        Ok(is_stale)
1706    }
1707
1708    /// Adds missing values to the msg object,
1709    /// writes the record to the database.
1710    ///
1711    /// If `update_msg_id` is set, that record is reused;
1712    /// if `update_msg_id` is None, a new record is created.
1713    async fn prepare_msg_raw(
1714        &mut self,
1715        context: &Context,
1716        msg: &mut Message,
1717        update_msg_id: Option<MsgId>,
1718    ) -> Result<()> {
1719        let mut to_id = 0;
1720        let mut location_id = 0;
1721
1722        if msg.rfc724_mid.is_empty() {
1723            msg.rfc724_mid = create_outgoing_rfc724_mid();
1724        }
1725
1726        if self.typ == Chattype::Single {
1727            if let Some(id) = context
1728                .sql
1729                .query_get_value(
1730                    "SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
1731                    (self.id,),
1732                )
1733                .await?
1734            {
1735                to_id = id;
1736            } else {
1737                error!(
1738                    context,
1739                    "Cannot send message, contact for {} not found.", self.id,
1740                );
1741                bail!("Cannot set message, contact for {} not found.", self.id);
1742            }
1743        } else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
1744            && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
1745        {
1746            msg.param.set_int(Param::AttachGroupImage, 1);
1747            self.param
1748                .remove(Param::Unpromoted)
1749                .set_i64(Param::GroupNameTimestamp, msg.timestamp_sort);
1750            self.update_param(context).await?;
1751            // TODO: Remove this compat code needed because Core <= v1.143:
1752            // - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
1753            //   send them when the group is promoted.
1754            // - doesn't sync QR code tokens for unpromoted groups and the group might be created
1755            //   before an upgrade.
1756            context
1757                .sync_qr_code_tokens(Some(self.grpid.as_str()))
1758                .await
1759                .log_err(context)
1760                .ok();
1761        }
1762
1763        let is_bot = context.get_config_bool(Config::Bot).await?;
1764        msg.param
1765            .set_optional(Param::Bot, Some("1").filter(|_| is_bot));
1766
1767        // Set "In-Reply-To:" to identify the message to which the composed message is a reply.
1768        // Set "References:" to identify the "thread" of the conversation.
1769        // Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
1770        let new_references;
1771        if self.is_self_talk() {
1772            // As self-talks are mainly used to transfer data between devices,
1773            // we do not set In-Reply-To/References in this case.
1774            new_references = String::new();
1775        } else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
1776            // We don't filter `OutPending` and `OutFailed` messages because the new message for
1777            // which `parent_query()` is done may assume that it will be received in a context
1778            // affected by those messages, e.g. they could add new members to a group and the
1779            // new message will contain them in "To:". Anyway recipients must be prepared to
1780            // orphaned references.
1781            self
1782                .id
1783                .get_parent_mime_headers(context, MessageState::OutPending)
1784                .await?
1785        {
1786            // "In-Reply-To:" is not changed if it is set manually.
1787            // This does not affect "References:" header, it will contain "default parent" (the
1788            // latest message in the thread) anyway.
1789            if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
1790                msg.in_reply_to = Some(parent_rfc724_mid.clone());
1791            }
1792
1793            // Use parent `In-Reply-To` as a fallback
1794            // in case parent message has no `References` header
1795            // as specified in RFC 5322:
1796            // > If the parent message does not contain
1797            // > a "References:" field but does have an "In-Reply-To:" field
1798            // > containing a single message identifier, then the "References:" field
1799            // > will contain the contents of the parent's "In-Reply-To:" field
1800            // > followed by the contents of the parent's "Message-ID:" field (if
1801            // > any).
1802            let parent_references = if parent_references.is_empty() {
1803                parent_in_reply_to
1804            } else {
1805                parent_references
1806            };
1807
1808            // The whole list of messages referenced may be huge.
1809            // Only take 2 recent references and add third from `In-Reply-To`.
1810            let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
1811            references_vec.reverse();
1812
1813            if !parent_rfc724_mid.is_empty()
1814                && !references_vec.contains(&parent_rfc724_mid.as_str())
1815            {
1816                references_vec.push(&parent_rfc724_mid)
1817            }
1818
1819            if references_vec.is_empty() {
1820                // As a fallback, use our Message-ID,
1821                // same as in the case of top-level message.
1822                new_references = msg.rfc724_mid.clone();
1823            } else {
1824                new_references = references_vec.join(" ");
1825            }
1826        } else {
1827            // This is a top-level message.
1828            // Add our Message-ID as first references.
1829            // This allows us to identify replies to our message even if
1830            // email server such as Outlook changes `Message-ID:` header.
1831            // MUAs usually keep the first Message-ID in `References:` header unchanged.
1832            new_references = msg.rfc724_mid.clone();
1833        }
1834
1835        // add independent location to database
1836        if msg.param.exists(Param::SetLatitude) {
1837            if let Ok(row_id) = context
1838                .sql
1839                .insert(
1840                    "INSERT INTO locations \
1841                     (timestamp,from_id,chat_id, latitude,longitude,independent)\
1842                     VALUES (?,?,?, ?,?,1);",
1843                    (
1844                        msg.timestamp_sort,
1845                        ContactId::SELF,
1846                        self.id,
1847                        msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
1848                        msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
1849                    ),
1850                )
1851                .await
1852            {
1853                location_id = row_id;
1854            }
1855        }
1856
1857        let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
1858            EphemeralTimer::Disabled
1859        } else {
1860            self.id.get_ephemeral_timer(context).await?
1861        };
1862        let ephemeral_timestamp = match ephemeral_timer {
1863            EphemeralTimer::Disabled => 0,
1864            EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
1865        };
1866
1867        let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
1868        let new_mime_headers = if msg.has_html() {
1869            if msg.param.exists(Param::Forwarded) {
1870                msg.get_id().get_html(context).await?
1871            } else {
1872                msg.param.get(Param::SendHtml).map(|s| s.to_string())
1873            }
1874        } else {
1875            None
1876        };
1877        let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
1878            let html_part = MimePart::new("text/html", s);
1879            let mut buffer = Vec::new();
1880            let cursor = Cursor::new(&mut buffer);
1881            html_part.write_part(cursor).ok();
1882            String::from_utf8_lossy(&buffer).to_string()
1883        });
1884        let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
1885            // We need to add some headers so that they are stripped before formatting HTML by
1886            // `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
1887            // anyway a useful metadata about the stored text.
1888            true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text),
1889            false => None,
1890        });
1891        let new_mime_headers = match new_mime_headers {
1892            Some(h) => Some(tokio::task::block_in_place(move || {
1893                buf_compress(h.as_bytes())
1894            })?),
1895            None => None,
1896        };
1897
1898        msg.chat_id = self.id;
1899        msg.from_id = ContactId::SELF;
1900
1901        // add message to the database
1902        if let Some(update_msg_id) = update_msg_id {
1903            context
1904                .sql
1905                .execute(
1906                    "UPDATE msgs
1907                     SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
1908                         state=?, txt=?, txt_normalized=?, subject=?, param=?,
1909                         hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
1910                         mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
1911                         ephemeral_timestamp=?
1912                     WHERE id=?;",
1913                    params_slice![
1914                        msg.rfc724_mid,
1915                        msg.chat_id,
1916                        msg.from_id,
1917                        to_id,
1918                        msg.timestamp_sort,
1919                        msg.viewtype,
1920                        msg.state,
1921                        msg_text,
1922                        message::normalize_text(&msg_text),
1923                        &msg.subject,
1924                        msg.param.to_string(),
1925                        msg.hidden,
1926                        msg.in_reply_to.as_deref().unwrap_or_default(),
1927                        new_references,
1928                        new_mime_headers.is_some(),
1929                        new_mime_headers.unwrap_or_default(),
1930                        location_id as i32,
1931                        ephemeral_timer,
1932                        ephemeral_timestamp,
1933                        update_msg_id
1934                    ],
1935                )
1936                .await?;
1937            msg.id = update_msg_id;
1938        } else {
1939            let raw_id = context
1940                .sql
1941                .insert(
1942                    "INSERT INTO msgs (
1943                        rfc724_mid,
1944                        chat_id,
1945                        from_id,
1946                        to_id,
1947                        timestamp,
1948                        type,
1949                        state,
1950                        txt,
1951                        txt_normalized,
1952                        subject,
1953                        param,
1954                        hidden,
1955                        mime_in_reply_to,
1956                        mime_references,
1957                        mime_modified,
1958                        mime_headers,
1959                        mime_compressed,
1960                        location_id,
1961                        ephemeral_timer,
1962                        ephemeral_timestamp)
1963                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
1964                    params_slice![
1965                        msg.rfc724_mid,
1966                        msg.chat_id,
1967                        msg.from_id,
1968                        to_id,
1969                        msg.timestamp_sort,
1970                        msg.viewtype,
1971                        msg.state,
1972                        msg_text,
1973                        message::normalize_text(&msg_text),
1974                        &msg.subject,
1975                        msg.param.to_string(),
1976                        msg.hidden,
1977                        msg.in_reply_to.as_deref().unwrap_or_default(),
1978                        new_references,
1979                        new_mime_headers.is_some(),
1980                        new_mime_headers.unwrap_or_default(),
1981                        location_id as i32,
1982                        ephemeral_timer,
1983                        ephemeral_timestamp
1984                    ],
1985                )
1986                .await?;
1987            context.new_msgs_notify.notify_one();
1988            msg.id = MsgId::new(u32::try_from(raw_id)?);
1989
1990            maybe_set_logging_xdc(context, msg, self.id).await?;
1991            context
1992                .update_webxdc_integration_database(msg, context)
1993                .await?;
1994        }
1995        context.scheduler.interrupt_ephemeral_task().await;
1996        Ok(())
1997    }
1998
1999    /// Sends a `SyncAction` synchronising chat contacts to other devices.
2000    pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
2001        if self.is_encrypted(context).await? {
2002            let self_fp = self_fingerprint(context).await?;
2003            let fingerprint_addrs = context
2004                .sql
2005                .query_map_vec(
2006                    "SELECT c.id, c.fingerprint, c.addr
2007                     FROM contacts c INNER JOIN chats_contacts cc
2008                     ON c.id=cc.contact_id
2009                     WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
2010                    (self.id,),
2011                    |row| {
2012                        if row.get::<_, ContactId>(0)? == ContactId::SELF {
2013                            return Ok((self_fp.to_string(), String::new()));
2014                        }
2015                        let fingerprint = row.get(1)?;
2016                        let addr = row.get(2)?;
2017                        Ok((fingerprint, addr))
2018                    },
2019                )
2020                .await?;
2021            self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs))
2022                .await?;
2023        } else {
2024            let addrs = context
2025                .sql
2026                .query_map_vec(
2027                    "SELECT c.addr \
2028                    FROM contacts c INNER JOIN chats_contacts cc \
2029                    ON c.id=cc.contact_id \
2030                    WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
2031                    (self.id,),
2032                    |row| row.get::<_, String>(0),
2033                )
2034                .await?;
2035            self.sync(context, SyncAction::SetContacts(addrs)).await?;
2036        }
2037        Ok(())
2038    }
2039
2040    /// Returns chat id for the purpose of synchronisation across devices.
2041    async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
2042        match self.typ {
2043            Chattype::Single => {
2044                if self.is_device_talk() {
2045                    return Ok(Some(SyncId::Device));
2046                }
2047
2048                let mut r = None;
2049                for contact_id in get_chat_contacts(context, self.id).await? {
2050                    if contact_id == ContactId::SELF && !self.is_self_talk() {
2051                        continue;
2052                    }
2053                    if r.is_some() {
2054                        return Ok(None);
2055                    }
2056                    let contact = Contact::get_by_id(context, contact_id).await?;
2057                    if let Some(fingerprint) = contact.fingerprint() {
2058                        r = Some(SyncId::ContactFingerprint(fingerprint.hex()));
2059                    } else {
2060                        r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
2061                    }
2062                }
2063                Ok(r)
2064            }
2065            Chattype::OutBroadcast
2066            | Chattype::InBroadcast
2067            | Chattype::Group
2068            | Chattype::Mailinglist => {
2069                if !self.grpid.is_empty() {
2070                    return Ok(Some(SyncId::Grpid(self.grpid.clone())));
2071                }
2072
2073                let Some((parent_rfc724_mid, parent_in_reply_to, _)) = self
2074                    .id
2075                    .get_parent_mime_headers(context, MessageState::OutDelivered)
2076                    .await?
2077                else {
2078                    warn!(
2079                        context,
2080                        "Chat::get_sync_id({}): No good message identifying the chat found.",
2081                        self.id
2082                    );
2083                    return Ok(None);
2084                };
2085                Ok(Some(SyncId::Msgids(vec![
2086                    parent_in_reply_to,
2087                    parent_rfc724_mid,
2088                ])))
2089            }
2090        }
2091    }
2092
2093    /// Synchronises a chat action to other devices.
2094    pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> Result<()> {
2095        if let Some(id) = self.get_sync_id(context).await? {
2096            sync(context, id, action).await?;
2097        }
2098        Ok(())
2099    }
2100}
2101
2102pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
2103    context
2104        .add_sync_item(SyncData::AlterChat { id, action })
2105        .await?;
2106    context.scheduler.interrupt_inbox().await;
2107    Ok(())
2108}
2109
2110/// Whether the chat is pinned or archived.
2111#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
2112#[repr(i8)]
2113pub enum ChatVisibility {
2114    /// Chat is neither archived nor pinned.
2115    Normal = 0,
2116
2117    /// Chat is archived.
2118    Archived = 1,
2119
2120    /// Chat is pinned to the top of the chatlist.
2121    Pinned = 2,
2122}
2123
2124impl rusqlite::types::ToSql for ChatVisibility {
2125    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
2126        let val = rusqlite::types::Value::Integer(*self as i64);
2127        let out = rusqlite::types::ToSqlOutput::Owned(val);
2128        Ok(out)
2129    }
2130}
2131
2132impl rusqlite::types::FromSql for ChatVisibility {
2133    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
2134        i64::column_result(value).map(|val| {
2135            match val {
2136                2 => ChatVisibility::Pinned,
2137                1 => ChatVisibility::Archived,
2138                0 => ChatVisibility::Normal,
2139                // fallback to Normal for unknown values, may happen eg. on imports created by a newer version.
2140                _ => ChatVisibility::Normal,
2141            }
2142        })
2143    }
2144}
2145
2146/// The current state of a chat.
2147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2148#[non_exhaustive]
2149pub struct ChatInfo {
2150    /// The chat ID.
2151    pub id: ChatId,
2152
2153    /// The type of chat as a `u32` representation of [Chattype].
2154    ///
2155    /// On the C API this number is one of the
2156    /// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
2157    /// or `DC_CHAT_TYPE_GROUP`
2158    /// constants.
2159    #[serde(rename = "type")]
2160    pub type_: u32,
2161
2162    /// The name of the chat.
2163    pub name: String,
2164
2165    /// Whether the chat is archived.
2166    pub archived: bool,
2167
2168    /// The "params" of the chat.
2169    ///
2170    /// This is the string-serialised version of `Params` currently.
2171    pub param: String,
2172
2173    /// Whether this chat is currently sending location-stream messages.
2174    pub is_sending_locations: bool,
2175
2176    /// Colour this chat should be represented in by the UI.
2177    ///
2178    /// Yes, spelling colour is hard.
2179    pub color: u32,
2180
2181    /// The path to the profile image.
2182    ///
2183    /// If there is no profile image set this will be an empty string
2184    /// currently.
2185    pub profile_image: std::path::PathBuf,
2186
2187    /// The draft message text.
2188    ///
2189    /// If the chat has not draft this is an empty string.
2190    ///
2191    /// TODO: This doesn't seem rich enough, it can not handle drafts
2192    ///       which contain non-text parts.  Perhaps it should be a
2193    ///       simple `has_draft` bool instead.
2194    pub draft: String,
2195
2196    /// Whether the chat is muted
2197    ///
2198    /// The exact time its muted can be found out via the `chat.mute_duration` property
2199    pub is_muted: bool,
2200
2201    /// Ephemeral message timer.
2202    pub ephemeral_timer: EphemeralTimer,
2203    // ToDo:
2204    // - [ ] summary,
2205    // - [ ] lastUpdated,
2206    // - [ ] freshMessageCounter,
2207    // - [ ] email
2208}
2209
2210async fn get_asset_icon(context: &Context, name: &str, bytes: &[u8]) -> Result<PathBuf> {
2211    ensure!(name.starts_with("icon-"));
2212    if let Some(icon) = context.sql.get_raw_config(name).await? {
2213        return Ok(get_abs_path(context, Path::new(&icon)));
2214    }
2215
2216    let blob =
2217        BlobObject::create_and_deduplicate_from_bytes(context, bytes, &format!("{name}.png"))?;
2218    let icon = blob.as_name().to_string();
2219    context.sql.set_raw_config(name, Some(&icon)).await?;
2220
2221    Ok(get_abs_path(context, Path::new(&icon)))
2222}
2223
2224pub(crate) async fn get_saved_messages_icon(context: &Context) -> Result<PathBuf> {
2225    get_asset_icon(
2226        context,
2227        "icon-saved-messages",
2228        include_bytes!("../assets/icon-saved-messages.png"),
2229    )
2230    .await
2231}
2232
2233pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
2234    get_asset_icon(
2235        context,
2236        "icon-device",
2237        include_bytes!("../assets/icon-device.png"),
2238    )
2239    .await
2240}
2241
2242pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
2243    get_asset_icon(
2244        context,
2245        "icon-archive",
2246        include_bytes!("../assets/icon-archive.png"),
2247    )
2248    .await
2249}
2250
2251/// Returns path to the icon
2252/// indicating unencrypted chats and address-contacts.
2253pub(crate) async fn get_unencrypted_icon(context: &Context) -> Result<PathBuf> {
2254    get_asset_icon(
2255        context,
2256        "icon-unencrypted",
2257        include_bytes!("../assets/icon-unencrypted.png"),
2258    )
2259    .await
2260}
2261
2262async fn update_special_chat_name(
2263    context: &Context,
2264    contact_id: ContactId,
2265    name: String,
2266) -> Result<()> {
2267    if let Some(ChatIdBlocked { id: chat_id, .. }) =
2268        ChatIdBlocked::lookup_by_contact(context, contact_id).await?
2269    {
2270        // the `!= name` condition avoids unneeded writes
2271        context
2272            .sql
2273            .execute(
2274                "UPDATE chats SET name=? WHERE id=? AND name!=?",
2275                (&name, chat_id, &name),
2276            )
2277            .await?;
2278    }
2279    Ok(())
2280}
2281
2282pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
2283    update_special_chat_name(
2284        context,
2285        ContactId::DEVICE,
2286        stock_str::device_messages(context).await,
2287    )
2288    .await?;
2289    update_special_chat_name(
2290        context,
2291        ContactId::SELF,
2292        stock_str::saved_messages(context).await,
2293    )
2294    .await?;
2295    Ok(())
2296}
2297
2298/// Handle a [`ChatId`] and its [`Blocked`] status at once.
2299///
2300/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
2301/// from the database.  It [`Deref`]s to [`ChatId`] so it can be used as an extension to
2302/// [`ChatId`].
2303///
2304/// [`Deref`]: std::ops::Deref
2305#[derive(Debug)]
2306pub(crate) struct ChatIdBlocked {
2307    /// Chat ID.
2308    pub id: ChatId,
2309
2310    /// Whether the chat is blocked, unblocked or a contact request.
2311    pub blocked: Blocked,
2312}
2313
2314impl ChatIdBlocked {
2315    /// Searches the database for the 1:1 chat with this contact.
2316    ///
2317    /// If no chat is found `None` is returned.
2318    pub async fn lookup_by_contact(
2319        context: &Context,
2320        contact_id: ContactId,
2321    ) -> Result<Option<Self>> {
2322        ensure!(context.sql.is_open().await, "Database not available");
2323        ensure!(
2324            contact_id != ContactId::UNDEFINED,
2325            "Invalid contact id requested"
2326        );
2327
2328        context
2329            .sql
2330            .query_row_optional(
2331                "SELECT c.id, c.blocked
2332                   FROM chats c
2333                  INNER JOIN chats_contacts j
2334                          ON c.id=j.chat_id
2335                  WHERE c.type=100  -- 100 = Chattype::Single
2336                    AND c.id>9      -- 9 = DC_CHAT_ID_LAST_SPECIAL
2337                    AND j.contact_id=?;",
2338                (contact_id,),
2339                |row| {
2340                    let id: ChatId = row.get(0)?;
2341                    let blocked: Blocked = row.get(1)?;
2342                    Ok(ChatIdBlocked { id, blocked })
2343                },
2344            )
2345            .await
2346    }
2347
2348    /// Returns the chat for the 1:1 chat with this contact.
2349    ///
2350    /// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
2351    /// state.
2352    pub async fn get_for_contact(
2353        context: &Context,
2354        contact_id: ContactId,
2355        create_blocked: Blocked,
2356    ) -> Result<Self> {
2357        ensure!(context.sql.is_open().await, "Database not available");
2358        ensure!(
2359            contact_id != ContactId::UNDEFINED,
2360            "Invalid contact id requested"
2361        );
2362
2363        if let Some(res) = Self::lookup_by_contact(context, contact_id).await? {
2364            // Already exists, no need to create.
2365            return Ok(res);
2366        }
2367
2368        let contact = Contact::get_by_id(context, contact_id).await?;
2369        let chat_name = contact.get_display_name().to_string();
2370        let mut params = Params::new();
2371        match contact_id {
2372            ContactId::SELF => {
2373                params.set_int(Param::Selftalk, 1);
2374            }
2375            ContactId::DEVICE => {
2376                params.set_int(Param::Devicetalk, 1);
2377            }
2378            _ => (),
2379        }
2380
2381        let smeared_time = create_smeared_timestamp(context);
2382
2383        let chat_id = context
2384            .sql
2385            .transaction(move |transaction| {
2386                transaction.execute(
2387                    "INSERT INTO chats
2388                     (type, name, param, blocked, created_timestamp)
2389                     VALUES(?, ?, ?, ?, ?)",
2390                    (
2391                        Chattype::Single,
2392                        chat_name,
2393                        params.to_string(),
2394                        create_blocked as u8,
2395                        smeared_time,
2396                    ),
2397                )?;
2398                let chat_id = ChatId::new(
2399                    transaction
2400                        .last_insert_rowid()
2401                        .try_into()
2402                        .context("chat table rowid overflows u32")?,
2403                );
2404
2405                transaction.execute(
2406                    "INSERT INTO chats_contacts
2407                 (chat_id, contact_id)
2408                 VALUES((SELECT last_insert_rowid()), ?)",
2409                    (contact_id,),
2410                )?;
2411
2412                Ok(chat_id)
2413            })
2414            .await?;
2415
2416        let chat = Chat::load_from_db(context, chat_id).await?;
2417        if chat.is_encrypted(context).await?
2418            && !chat.param.exists(Param::Devicetalk)
2419            && !chat.param.exists(Param::Selftalk)
2420        {
2421            chat_id.add_encrypted_msg(context, smeared_time).await?;
2422        }
2423
2424        Ok(Self {
2425            id: chat_id,
2426            blocked: create_blocked,
2427        })
2428    }
2429}
2430
2431async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
2432    if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::Call {
2433        // the caller should check if the message text is empty
2434    } else if msg.viewtype.has_file() {
2435        let viewtype_orig = msg.viewtype;
2436        let mut blob = msg
2437            .param
2438            .get_file_blob(context)?
2439            .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
2440        let mut maybe_image = false;
2441
2442        if msg.viewtype == Viewtype::File
2443            || msg.viewtype == Viewtype::Image
2444            || msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
2445        {
2446            // Correct the type, take care not to correct already very special
2447            // formats as GIF or VOICE.
2448            //
2449            // Typical conversions:
2450            // - from FILE to AUDIO/VIDEO/IMAGE
2451            // - from FILE/IMAGE to GIF */
2452            if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
2453                if msg.viewtype == Viewtype::Sticker {
2454                    if better_type != Viewtype::Image {
2455                        // UIs don't want conversions of `Sticker` to anything other than `Image`.
2456                        msg.param.set_int(Param::ForceSticker, 1);
2457                    }
2458                } else if better_type == Viewtype::Image {
2459                    maybe_image = true;
2460                } else if better_type != Viewtype::Webxdc
2461                    || context
2462                        .ensure_sendable_webxdc_file(&blob.to_abs_path())
2463                        .await
2464                        .is_ok()
2465                {
2466                    msg.viewtype = better_type;
2467                }
2468            }
2469        } else if msg.viewtype == Viewtype::Webxdc {
2470            context
2471                .ensure_sendable_webxdc_file(&blob.to_abs_path())
2472                .await?;
2473        }
2474
2475        if msg.viewtype == Viewtype::Vcard {
2476            msg.try_set_vcard(context, &blob.to_abs_path()).await?;
2477        }
2478        if msg.viewtype == Viewtype::File && maybe_image
2479            || msg.viewtype == Viewtype::Image
2480            || msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
2481        {
2482            let new_name = blob
2483                .check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
2484                .await?;
2485            msg.param.set(Param::Filename, new_name);
2486            msg.param.set(Param::File, blob.as_name());
2487        }
2488
2489        if !msg.param.exists(Param::MimeType) {
2490            if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
2491                // If we unexpectedly didn't recognize the file as image, don't send it as such,
2492                // either the format is unsupported or the image is corrupted.
2493                let mime = match viewtype != Viewtype::Image
2494                    || matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
2495                {
2496                    true => mime,
2497                    false => "application/octet-stream",
2498                };
2499                msg.param.set(Param::MimeType, mime);
2500            }
2501        }
2502
2503        msg.try_calc_and_set_dimensions(context).await?;
2504
2505        let filename = msg.get_filename().context("msg has no file")?;
2506        let suffix = Path::new(&filename)
2507            .extension()
2508            .and_then(|e| e.to_str())
2509            .unwrap_or("dat");
2510        // Get file name to use for sending. For privacy purposes, we do not transfer the original
2511        // filenames e.g. for images; these names are normally not needed and contain timestamps,
2512        // running numbers, etc.
2513        let filename: String = match viewtype_orig {
2514            Viewtype::Voice => format!(
2515                "voice-messsage_{}.{}",
2516                chrono::Utc
2517                    .timestamp_opt(msg.timestamp_sort, 0)
2518                    .single()
2519                    .map_or_else(
2520                        || "YY-mm-dd_hh:mm:ss".to_string(),
2521                        |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
2522                    ),
2523                &suffix
2524            ),
2525            Viewtype::Image | Viewtype::Gif => format!(
2526                "image_{}.{}",
2527                chrono::Utc
2528                    .timestamp_opt(msg.timestamp_sort, 0)
2529                    .single()
2530                    .map_or_else(
2531                        || "YY-mm-dd_hh:mm:ss".to_string(),
2532                        |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
2533                    ),
2534                &suffix,
2535            ),
2536            Viewtype::Video => format!(
2537                "video_{}.{}",
2538                chrono::Utc
2539                    .timestamp_opt(msg.timestamp_sort, 0)
2540                    .single()
2541                    .map_or_else(
2542                        || "YY-mm-dd_hh:mm:ss".to_string(),
2543                        |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
2544                    ),
2545                &suffix
2546            ),
2547            _ => filename,
2548        };
2549        msg.param.set(Param::Filename, filename);
2550
2551        info!(
2552            context,
2553            "Attaching \"{}\" for message type #{}.",
2554            blob.to_abs_path().display(),
2555            msg.viewtype
2556        );
2557    } else {
2558        bail!("Cannot send messages of type #{}.", msg.viewtype);
2559    }
2560    Ok(())
2561}
2562
2563/// Returns whether a contact is in a chat or not.
2564pub async fn is_contact_in_chat(
2565    context: &Context,
2566    chat_id: ChatId,
2567    contact_id: ContactId,
2568) -> Result<bool> {
2569    // this function works for group and for normal chats, however, it is more useful
2570    // for group chats.
2571    // ContactId::SELF may be used to check, if the user itself is in a group
2572    // chat (ContactId::SELF is not added to normal chats)
2573
2574    let exists = context
2575        .sql
2576        .exists(
2577            "SELECT COUNT(*) FROM chats_contacts
2578             WHERE chat_id=? AND contact_id=?
2579             AND add_timestamp >= remove_timestamp",
2580            (chat_id, contact_id),
2581        )
2582        .await?;
2583    Ok(exists)
2584}
2585
2586/// Sends a message object to a chat.
2587///
2588/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
2589/// However, this does not imply, the message really reached the recipient -
2590/// sending may be delayed eg. due to network problems. However, from your
2591/// view, you're done with the message. Sooner or later it will find its way.
2592pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
2593    ensure!(
2594        !chat_id.is_special(),
2595        "chat_id cannot be a special chat: {chat_id}"
2596    );
2597
2598    if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
2599        msg.param.remove(Param::GuaranteeE2ee);
2600        msg.param.remove(Param::ForcePlaintext);
2601        msg.update_param(context).await?;
2602    }
2603
2604    // protect all system messages against RTLO attacks
2605    if msg.is_system_message() {
2606        msg.text = sanitize_bidi_characters(&msg.text);
2607    }
2608
2609    if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
2610        if !msg.hidden {
2611            context.emit_msgs_changed(msg.chat_id, msg.id);
2612        }
2613
2614        if msg.param.exists(Param::SetLatitude) {
2615            context.emit_location_changed(Some(ContactId::SELF)).await?;
2616        }
2617
2618        context.scheduler.interrupt_smtp().await;
2619    }
2620
2621    Ok(msg.id)
2622}
2623
2624/// Tries to send a message synchronously.
2625///
2626/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
2627/// message. If this fails, the jobs remain in the database for later sending.
2628pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
2629    let rowids = prepare_send_msg(context, chat_id, msg).await?;
2630    if rowids.is_empty() {
2631        return Ok(msg.id);
2632    }
2633    let mut smtp = crate::smtp::Smtp::new();
2634    for rowid in rowids {
2635        send_msg_to_smtp(context, &mut smtp, rowid)
2636            .await
2637            .context("failed to send message, queued for later sending")?;
2638    }
2639    context.emit_msgs_changed(msg.chat_id, msg.id);
2640    Ok(msg.id)
2641}
2642
2643/// Prepares a message to be sent out.
2644///
2645/// Returns row ids of the `smtp` table.
2646async fn prepare_send_msg(
2647    context: &Context,
2648    chat_id: ChatId,
2649    msg: &mut Message,
2650) -> Result<Vec<i64>> {
2651    let mut chat = Chat::load_from_db(context, chat_id).await?;
2652
2653    let skip_fn = |reason: &CantSendReason| match reason {
2654        CantSendReason::ContactRequest => {
2655            // Allow securejoin messages, they are supposed to repair the verification.
2656            // If the chat is a contact request, let the user accept it later.
2657            msg.param.get_cmd() == SystemMessage::SecurejoinMessage
2658        }
2659        // Allow to send "Member removed" messages so we can leave the group/broadcast.
2660        // Necessary checks should be made anyway before removing contact
2661        // from the chat.
2662        CantSendReason::NotAMember | CantSendReason::InBroadcast => {
2663            msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
2664        }
2665        CantSendReason::MissingKey => msg
2666            .param
2667            .get_bool(Param::ForcePlaintext)
2668            .unwrap_or_default(),
2669        _ => false,
2670    };
2671    if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
2672        bail!("Cannot send to {chat_id}: {reason}");
2673    }
2674
2675    // Check a quote reply is not leaking data from other chats.
2676    // This is meant as a last line of defence, the UI should check that before as well.
2677    // (We allow Chattype::Single in general for "Reply Privately";
2678    // checking for exact contact_id will produce false positives when ppl just left the group)
2679    if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
2680        if let Some(quoted_message) = msg.quoted_message(context).await? {
2681            if quoted_message.chat_id != chat_id {
2682                bail!(
2683                    "Quote of message from {} cannot be sent to {chat_id}",
2684                    quoted_message.chat_id
2685                );
2686            }
2687        }
2688    }
2689
2690    // check current MessageState for drafts (to keep msg_id) ...
2691    let update_msg_id = if msg.state == MessageState::OutDraft {
2692        msg.hidden = false;
2693        if !msg.id.is_special() && msg.chat_id == chat_id {
2694            Some(msg.id)
2695        } else {
2696            None
2697        }
2698    } else {
2699        None
2700    };
2701
2702    // ... then change the MessageState in the message object
2703    msg.state = MessageState::OutPending;
2704
2705    msg.timestamp_sort = create_smeared_timestamp(context);
2706    prepare_msg_blob(context, msg).await?;
2707    if !msg.hidden {
2708        chat_id.unarchive_if_not_muted(context, msg.state).await?;
2709    }
2710    chat.prepare_msg_raw(context, msg, update_msg_id).await?;
2711
2712    let row_ids = create_send_msg_jobs(context, msg)
2713        .await
2714        .context("Failed to create send jobs")?;
2715    if !row_ids.is_empty() {
2716        donation_request_maybe(context).await.log_err(context).ok();
2717    }
2718    Ok(row_ids)
2719}
2720
2721/// Constructs jobs for sending a message and inserts them into the appropriate table.
2722///
2723/// Updates the message `GuaranteeE2ee` parameter and persists it
2724/// in the database depending on whether the message
2725/// is added to the outgoing queue as encrypted or not.
2726///
2727/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
2728///
2729/// The caller has to interrupt SMTP loop or otherwise process new rows.
2730pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
2731    if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
2732        msg.chat_id
2733            .update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
2734            .await?;
2735    }
2736
2737    let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
2738    let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await {
2739        Ok(mf) => mf,
2740        Err(err) => {
2741            // Mark message as failed
2742            message::set_msg_failed(context, msg, &err.to_string())
2743                .await
2744                .ok();
2745            return Err(err);
2746        }
2747    };
2748    let attach_selfavatar = mimefactory.attach_selfavatar;
2749    let mut recipients = mimefactory.recipients();
2750
2751    let from = context.get_primary_self_addr().await?;
2752    let lowercase_from = from.to_lowercase();
2753
2754    // Send BCC to self if it is enabled.
2755    //
2756    // Previous versions of Delta Chat did not send BCC self
2757    // if DeleteServerAfter was set to immediately delete messages
2758    // from the server. This is not the case anymore
2759    // because BCC-self messages are also used to detect
2760    // that message was sent if SMTP server is slow to respond
2761    // and connection is frequently lost
2762    // before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
2763    // disabled by default is fine.
2764    //
2765    // `from` must be the last addr, see `receive_imf_inner()` why.
2766    recipients.retain(|x| x.to_lowercase() != lowercase_from);
2767    if (context.get_config_bool(Config::BccSelf).await?
2768        || msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
2769        && (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
2770    {
2771        recipients.push(from);
2772    }
2773
2774    // Default Webxdc integrations are hidden messages and must not be sent out
2775    if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
2776        recipients.clear();
2777    }
2778
2779    if recipients.is_empty() {
2780        // may happen eg. for groups with only SELF and bcc_self disabled
2781        info!(
2782            context,
2783            "Message {} has no recipient, skipping smtp-send.", msg.id
2784        );
2785        msg.param.set_int(Param::GuaranteeE2ee, 1);
2786        msg.update_param(context).await?;
2787        msg.id.set_delivered(context).await?;
2788        msg.state = MessageState::OutDelivered;
2789        return Ok(Vec::new());
2790    }
2791
2792    let rendered_msg = match mimefactory.render(context).await {
2793        Ok(res) => Ok(res),
2794        Err(err) => {
2795            message::set_msg_failed(context, msg, &err.to_string()).await?;
2796            Err(err)
2797        }
2798    }?;
2799
2800    if needs_encryption && !rendered_msg.is_encrypted {
2801        /* unrecoverable */
2802        message::set_msg_failed(
2803            context,
2804            msg,
2805            "End-to-end-encryption unavailable unexpectedly.",
2806        )
2807        .await?;
2808        bail!(
2809            "e2e encryption unavailable {} - {:?}",
2810            msg.id,
2811            needs_encryption
2812        );
2813    }
2814
2815    let now = smeared_time(context);
2816
2817    if rendered_msg.last_added_location_id.is_some() {
2818        if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
2819            error!(context, "Failed to set kml sent_timestamp: {err:#}.");
2820        }
2821    }
2822
2823    if attach_selfavatar {
2824        if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
2825            error!(context, "Failed to set selfavatar timestamp: {err:#}.");
2826        }
2827    }
2828
2829    if rendered_msg.is_encrypted {
2830        msg.param.set_int(Param::GuaranteeE2ee, 1);
2831    } else {
2832        msg.param.remove(Param::GuaranteeE2ee);
2833    }
2834    msg.subject.clone_from(&rendered_msg.subject);
2835    context
2836        .sql
2837        .execute(
2838            "UPDATE msgs SET subject=?, param=? WHERE id=?",
2839            (&msg.subject, msg.param.to_string(), msg.id),
2840        )
2841        .await?;
2842
2843    let chunk_size = context.get_max_smtp_rcpt_to().await?;
2844    let trans_fn = |t: &mut rusqlite::Transaction| {
2845        let mut row_ids = Vec::<i64>::new();
2846        if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
2847            t.execute(
2848                &format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
2849                (),
2850            )?;
2851            t.execute(
2852                "INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
2853                (&rendered_msg.message, msg.id),
2854            )?;
2855        } else {
2856            for recipients_chunk in recipients.chunks(chunk_size) {
2857                let recipients_chunk = recipients_chunk.join(" ");
2858                let row_id = t.execute(
2859                    "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
2860                    VALUES            (?1,         ?2,         ?3,   ?4)",
2861                    (
2862                        &rendered_msg.rfc724_mid,
2863                        recipients_chunk,
2864                        &rendered_msg.message,
2865                        msg.id,
2866                    ),
2867                )?;
2868                row_ids.push(row_id.try_into()?);
2869            }
2870        }
2871        Ok(row_ids)
2872    };
2873    context.sql.transaction(trans_fn).await
2874}
2875
2876/// Sends a text message to the given chat.
2877///
2878/// Returns database ID of the sent message.
2879pub async fn send_text_msg(
2880    context: &Context,
2881    chat_id: ChatId,
2882    text_to_send: String,
2883) -> Result<MsgId> {
2884    ensure!(
2885        !chat_id.is_special(),
2886        "bad chat_id, can not be a special chat: {chat_id}"
2887    );
2888
2889    let mut msg = Message::new_text(text_to_send);
2890    send_msg(context, chat_id, &mut msg).await
2891}
2892
2893/// Sends chat members a request to edit the given message's text.
2894pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
2895    let mut original_msg = Message::load_from_db(context, msg_id).await?;
2896    ensure!(
2897        original_msg.from_id == ContactId::SELF,
2898        "Can edit only own messages"
2899    );
2900    ensure!(!original_msg.is_info(), "Cannot edit info messages");
2901    ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
2902    ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
2903    ensure!(
2904        !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
2905        "Cannot add text"
2906    );
2907    ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
2908    if original_msg.text == new_text {
2909        info!(context, "Text unchanged.");
2910        return Ok(());
2911    }
2912
2913    save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
2914
2915    let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
2916    edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
2917    if original_msg.get_showpadlock() {
2918        edit_msg.param.set_int(Param::GuaranteeE2ee, 1);
2919    }
2920    edit_msg
2921        .param
2922        .set(Param::TextEditFor, original_msg.rfc724_mid);
2923    edit_msg.hidden = true;
2924    send_msg(context, original_msg.chat_id, &mut edit_msg).await?;
2925    Ok(())
2926}
2927
2928pub(crate) async fn save_text_edit_to_db(
2929    context: &Context,
2930    original_msg: &mut Message,
2931    new_text: &str,
2932) -> Result<()> {
2933    original_msg.param.set_int(Param::IsEdited, 1);
2934    context
2935        .sql
2936        .execute(
2937            "UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
2938            (
2939                new_text,
2940                message::normalize_text(new_text),
2941                original_msg.param.to_string(),
2942                original_msg.id,
2943            ),
2944        )
2945        .await?;
2946    context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
2947    Ok(())
2948}
2949
2950async fn donation_request_maybe(context: &Context) -> Result<()> {
2951    let secs_between_checks = 30 * 24 * 60 * 60;
2952    let now = time();
2953    let ts = context
2954        .get_config_i64(Config::DonationRequestNextCheck)
2955        .await?;
2956    if ts > now {
2957        return Ok(());
2958    }
2959    let msg_cnt = context.sql.count(
2960        "SELECT COUNT(*) FROM msgs WHERE state>=? AND hidden=0",
2961        (MessageState::OutDelivered,),
2962    );
2963    let ts = if ts == 0 || msg_cnt.await? < 100 {
2964        now.saturating_add(secs_between_checks)
2965    } else {
2966        let mut msg = Message::new_text(stock_str::donation_request(context).await);
2967        add_device_msg(context, None, Some(&mut msg)).await?;
2968        i64::MAX
2969    };
2970    context
2971        .set_config_internal(Config::DonationRequestNextCheck, Some(&ts.to_string()))
2972        .await
2973}
2974
2975/// Chat message list request options.
2976#[derive(Debug)]
2977pub struct MessageListOptions {
2978    /// Return only info messages.
2979    pub info_only: bool,
2980
2981    /// Add day markers before each date regarding the local timezone.
2982    pub add_daymarker: bool,
2983}
2984
2985/// Returns all messages belonging to the chat.
2986pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<ChatItem>> {
2987    get_chat_msgs_ex(
2988        context,
2989        chat_id,
2990        MessageListOptions {
2991            info_only: false,
2992            add_daymarker: false,
2993        },
2994    )
2995    .await
2996}
2997
2998/// Returns messages belonging to the chat according to the given options.
2999pub async fn get_chat_msgs_ex(
3000    context: &Context,
3001    chat_id: ChatId,
3002    options: MessageListOptions,
3003) -> Result<Vec<ChatItem>> {
3004    let MessageListOptions {
3005        info_only,
3006        add_daymarker,
3007    } = options;
3008    let process_row = if info_only {
3009        |row: &rusqlite::Row| {
3010            // is_info logic taken from Message.is_info()
3011            let params = row.get::<_, String>("param")?;
3012            let (from_id, to_id) = (
3013                row.get::<_, ContactId>("from_id")?,
3014                row.get::<_, ContactId>("to_id")?,
3015            );
3016            let is_info_msg: bool = from_id == ContactId::INFO
3017                || to_id == ContactId::INFO
3018                || match Params::from_str(&params) {
3019                    Ok(p) => {
3020                        let cmd = p.get_cmd();
3021                        cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
3022                    }
3023                    _ => false,
3024                };
3025
3026            Ok((
3027                row.get::<_, i64>("timestamp")?,
3028                row.get::<_, MsgId>("id")?,
3029                !is_info_msg,
3030            ))
3031        }
3032    } else {
3033        |row: &rusqlite::Row| {
3034            Ok((
3035                row.get::<_, i64>("timestamp")?,
3036                row.get::<_, MsgId>("id")?,
3037                false,
3038            ))
3039        }
3040    };
3041    let process_rows = |rows: rusqlite::MappedRows<_>| {
3042        // It is faster to sort here rather than
3043        // let sqlite execute an ORDER BY clause.
3044        let mut sorted_rows = Vec::new();
3045        for row in rows {
3046            let (ts, curr_id, exclude_message): (i64, MsgId, bool) = row?;
3047            if !exclude_message {
3048                sorted_rows.push((ts, curr_id));
3049            }
3050        }
3051        sorted_rows.sort_unstable();
3052
3053        let mut ret = Vec::new();
3054        let mut last_day = 0;
3055        let cnv_to_local = gm2local_offset();
3056
3057        for (ts, curr_id) in sorted_rows {
3058            if add_daymarker {
3059                let curr_local_timestamp = ts + cnv_to_local;
3060                let secs_in_day = 86400;
3061                let curr_day = curr_local_timestamp / secs_in_day;
3062                if curr_day != last_day {
3063                    ret.push(ChatItem::DayMarker {
3064                        timestamp: curr_day * secs_in_day - cnv_to_local,
3065                    });
3066                    last_day = curr_day;
3067                }
3068            }
3069            ret.push(ChatItem::Message { msg_id: curr_id });
3070        }
3071        Ok(ret)
3072    };
3073
3074    let items = if info_only {
3075        context
3076            .sql
3077            .query_map(
3078        // GLOB is used here instead of LIKE because it is case-sensitive
3079                "SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
3080               FROM msgs m
3081              WHERE m.chat_id=?
3082                AND m.hidden=0
3083                AND (
3084                    m.param GLOB \"*S=*\"
3085                    OR m.from_id == ?
3086                    OR m.to_id == ?
3087                );",
3088                (chat_id, ContactId::INFO, ContactId::INFO),
3089                process_row,
3090                process_rows,
3091            )
3092            .await?
3093    } else {
3094        context
3095            .sql
3096            .query_map(
3097                "SELECT m.id AS id, m.timestamp AS timestamp
3098               FROM msgs m
3099              WHERE m.chat_id=?
3100                AND m.hidden=0;",
3101                (chat_id,),
3102                process_row,
3103                process_rows,
3104            )
3105            .await?
3106    };
3107    Ok(items)
3108}
3109
3110/// Marks all messages in the chat as noticed.
3111/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
3112pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
3113    // "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
3114    // the additional SELECT statement may speed up things as no write-blocking is needed.
3115    if chat_id.is_archived_link() {
3116        let chat_ids_in_archive = context
3117            .sql
3118            .query_map_vec(
3119                "SELECT DISTINCT(m.chat_id) FROM msgs m
3120                    LEFT JOIN chats c ON m.chat_id=c.id
3121                    WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.archived=1",
3122                (),
3123                |row| row.get::<_, ChatId>(0),
3124            )
3125            .await?;
3126        if chat_ids_in_archive.is_empty() {
3127            return Ok(());
3128        }
3129
3130        context
3131            .sql
3132            .transaction(|transaction| {
3133                let mut stmt = transaction.prepare(
3134                    "UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id = ?",
3135                )?;
3136                for chat_id_in_archive in &chat_ids_in_archive {
3137                    stmt.execute((chat_id_in_archive,))?;
3138                }
3139                Ok(())
3140            })
3141            .await?;
3142
3143        for chat_id_in_archive in chat_ids_in_archive {
3144            start_chat_ephemeral_timers(context, chat_id_in_archive).await?;
3145            context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
3146            chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
3147        }
3148    } else {
3149        start_chat_ephemeral_timers(context, chat_id).await?;
3150
3151        let noticed_msgs_count = context
3152            .sql
3153            .execute(
3154                "UPDATE msgs
3155            SET state=?
3156          WHERE state=?
3157            AND hidden=0
3158            AND chat_id=?;",
3159                (MessageState::InNoticed, MessageState::InFresh, chat_id),
3160            )
3161            .await?;
3162
3163        // This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
3164        // locally (i.e. when the chat was opened locally).
3165        let hidden_messages = context
3166            .sql
3167            .query_map_vec(
3168                "SELECT id, rfc724_mid FROM msgs
3169                    WHERE state=?
3170                      AND hidden=1
3171                      AND chat_id=?
3172                    ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
3173                (MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
3174                |row| {
3175                    let msg_id: MsgId = row.get(0)?;
3176                    let rfc724_mid: String = row.get(1)?;
3177                    Ok((msg_id, rfc724_mid))
3178                },
3179            )
3180            .await?;
3181        for (msg_id, rfc724_mid) in &hidden_messages {
3182            message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
3183            imap::markseen_on_imap_table(context, rfc724_mid).await?;
3184        }
3185
3186        if noticed_msgs_count == 0 {
3187            return Ok(());
3188        }
3189    }
3190
3191    context.emit_event(EventType::MsgsNoticed(chat_id));
3192    chatlist_events::emit_chatlist_item_changed(context, chat_id);
3193    context.on_archived_chats_maybe_noticed();
3194    Ok(())
3195}
3196
3197/// Marks messages preceding outgoing messages as noticed.
3198///
3199/// In a chat, if there is an outgoing message, it can be assumed that all previous
3200/// messages were noticed. So, this function takes a Vec of messages that were
3201/// just received, and for all the outgoing messages, it marks all
3202/// previous messages as noticed.
3203pub(crate) async fn mark_old_messages_as_noticed(
3204    context: &Context,
3205    mut msgs: Vec<ReceivedMsg>,
3206) -> Result<()> {
3207    msgs.retain(|m| m.state.is_outgoing());
3208    if msgs.is_empty() {
3209        return Ok(());
3210    }
3211
3212    let mut msgs_by_chat: HashMap<ChatId, ReceivedMsg> = HashMap::new();
3213    for msg in msgs {
3214        let chat_id = msg.chat_id;
3215        if let Some(existing_msg) = msgs_by_chat.get(&chat_id) {
3216            if msg.sort_timestamp > existing_msg.sort_timestamp {
3217                msgs_by_chat.insert(chat_id, msg);
3218            }
3219        } else {
3220            msgs_by_chat.insert(chat_id, msg);
3221        }
3222    }
3223
3224    let changed_chats = context
3225        .sql
3226        .transaction(|transaction| {
3227            let mut changed_chats = Vec::new();
3228            for (_, msg) in msgs_by_chat {
3229                let changed_rows = transaction.execute(
3230                    "UPDATE msgs
3231            SET state=?
3232          WHERE state=?
3233            AND hidden=0
3234            AND chat_id=?
3235            AND timestamp<=?;",
3236                    (
3237                        MessageState::InNoticed,
3238                        MessageState::InFresh,
3239                        msg.chat_id,
3240                        msg.sort_timestamp,
3241                    ),
3242                )?;
3243                if changed_rows > 0 {
3244                    changed_chats.push(msg.chat_id);
3245                }
3246            }
3247            Ok(changed_chats)
3248        })
3249        .await?;
3250
3251    if !changed_chats.is_empty() {
3252        info!(
3253            context,
3254            "Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
3255        );
3256        context.on_archived_chats_maybe_noticed();
3257    }
3258
3259    for c in changed_chats {
3260        start_chat_ephemeral_timers(context, c).await?;
3261        context.emit_event(EventType::MsgsNoticed(c));
3262        chatlist_events::emit_chatlist_item_changed(context, c);
3263    }
3264
3265    Ok(())
3266}
3267
3268/// Returns all database message IDs of the given types.
3269///
3270/// If `chat_id` is None, return messages from any chat.
3271///
3272/// `Viewtype::Unknown` can be used for `msg_type2` and `msg_type3`
3273/// if less than 3 viewtypes are requested.
3274pub async fn get_chat_media(
3275    context: &Context,
3276    chat_id: Option<ChatId>,
3277    msg_type: Viewtype,
3278    msg_type2: Viewtype,
3279    msg_type3: Viewtype,
3280) -> Result<Vec<MsgId>> {
3281    let list = if msg_type == Viewtype::Webxdc
3282        && msg_type2 == Viewtype::Unknown
3283        && msg_type3 == Viewtype::Unknown
3284    {
3285        context
3286            .sql
3287            .query_map_vec(
3288                "SELECT id
3289               FROM msgs
3290              WHERE (1=? OR chat_id=?)
3291                AND chat_id != ?
3292                AND type = ?
3293                AND hidden=0
3294              ORDER BY max(timestamp, timestamp_rcvd), id;",
3295                (
3296                    chat_id.is_none(),
3297                    chat_id.unwrap_or_else(|| ChatId::new(0)),
3298                    DC_CHAT_ID_TRASH,
3299                    Viewtype::Webxdc,
3300                ),
3301                |row| row.get::<_, MsgId>(0),
3302            )
3303            .await?
3304    } else {
3305        context
3306            .sql
3307            .query_map_vec(
3308                "SELECT id
3309               FROM msgs
3310              WHERE (1=? OR chat_id=?)
3311                AND chat_id != ?
3312                AND type IN (?, ?, ?)
3313                AND hidden=0
3314              ORDER BY timestamp, id;",
3315                (
3316                    chat_id.is_none(),
3317                    chat_id.unwrap_or_else(|| ChatId::new(0)),
3318                    DC_CHAT_ID_TRASH,
3319                    msg_type,
3320                    if msg_type2 != Viewtype::Unknown {
3321                        msg_type2
3322                    } else {
3323                        msg_type
3324                    },
3325                    if msg_type3 != Viewtype::Unknown {
3326                        msg_type3
3327                    } else {
3328                        msg_type
3329                    },
3330                ),
3331                |row| row.get::<_, MsgId>(0),
3332            )
3333            .await?
3334    };
3335    Ok(list)
3336}
3337
3338/// Returns a vector of contact IDs for given chat ID.
3339pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
3340    // Normal chats do not include SELF.  Group chats do (as it may happen that one is deleted from a
3341    // groupchat but the chats stays visible, moreover, this makes displaying lists easier)
3342    context
3343        .sql
3344        .query_map_vec(
3345            "SELECT cc.contact_id
3346               FROM chats_contacts cc
3347               LEFT JOIN contacts c
3348                      ON c.id=cc.contact_id
3349              WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp
3350              ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
3351            (chat_id,),
3352            |row| row.get::<_, ContactId>(0),
3353        )
3354        .await
3355}
3356
3357/// Returns a vector of contact IDs for given chat ID that are no longer part of the group.
3358///
3359/// Members that have been removed recently are in the beginning of the list.
3360pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
3361    let now = time();
3362    context
3363        .sql
3364        .query_map_vec(
3365            "SELECT cc.contact_id
3366             FROM chats_contacts cc
3367             LEFT JOIN contacts c
3368                  ON c.id=cc.contact_id
3369             WHERE cc.chat_id=?
3370             AND cc.add_timestamp < cc.remove_timestamp
3371             AND ? < cc.remove_timestamp
3372             ORDER BY c.id=1, cc.remove_timestamp DESC, c.id DESC",
3373            (chat_id, now.saturating_sub(60 * 24 * 3600)),
3374            |row| row.get::<_, ContactId>(0),
3375        )
3376        .await
3377}
3378
3379/// Creates an encrypted group chat.
3380pub async fn create_group(context: &Context, name: &str) -> Result<ChatId> {
3381    create_group_ex(context, Sync, create_id(), name).await
3382}
3383
3384/// Creates an unencrypted group chat.
3385pub async fn create_group_unencrypted(context: &Context, name: &str) -> Result<ChatId> {
3386    create_group_ex(context, Sync, String::new(), name).await
3387}
3388
3389/// Creates a group chat.
3390///
3391/// * `sync` - Whether a multi-device synchronization message should be sent. Ignored for
3392///   unencrypted chats currently.
3393/// * `grpid` - Group ID. Iff nonempty, the chat is encrypted (with key-contacts).
3394/// * `name` - Chat name.
3395pub(crate) async fn create_group_ex(
3396    context: &Context,
3397    sync: sync::Sync,
3398    grpid: String,
3399    name: &str,
3400) -> Result<ChatId> {
3401    let mut chat_name = sanitize_single_line(name);
3402    if chat_name.is_empty() {
3403        // We can't just fail because the user would lose the work already done in the UI like
3404        // selecting members.
3405        error!(context, "Invalid chat name: {name}.");
3406        chat_name = "…".to_string();
3407    }
3408
3409    let timestamp = create_smeared_timestamp(context);
3410    let row_id = context
3411        .sql
3412        .insert(
3413            "INSERT INTO chats
3414        (type, name, grpid, param, created_timestamp)
3415        VALUES(?, ?, ?, \'U=1\', ?);",
3416            (Chattype::Group, &chat_name, &grpid, timestamp),
3417        )
3418        .await?;
3419
3420    let chat_id = ChatId::new(u32::try_from(row_id)?);
3421    add_to_chat_contacts_table(context, timestamp, chat_id, &[ContactId::SELF]).await?;
3422
3423    context.emit_msgs_changed_without_ids();
3424    chatlist_events::emit_chatlist_changed(context);
3425    chatlist_events::emit_chatlist_item_changed(context, chat_id);
3426
3427    if !grpid.is_empty() {
3428        // Add "Messages are end-to-end encrypted." message.
3429        chat_id.add_encrypted_msg(context, timestamp).await?;
3430    }
3431
3432    if !context.get_config_bool(Config::Bot).await?
3433        && !context.get_config_bool(Config::SkipStartMessages).await?
3434    {
3435        let text = stock_str::new_group_send_first_message(context).await;
3436        add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
3437    }
3438    if let (true, true) = (sync.into(), !grpid.is_empty()) {
3439        let id = SyncId::Grpid(grpid);
3440        let action = SyncAction::CreateGroupEncrypted(chat_name);
3441        self::sync(context, id, action).await.log_err(context).ok();
3442    }
3443    Ok(chat_id)
3444}
3445
3446/// Create a new **broadcast channel**
3447/// (called "Channel" in the UI).
3448///
3449/// Broadcast channels are similar to groups on the sending device,
3450/// however, recipients get the messages in a read-only chat
3451/// and will not see who the other members are.
3452///
3453/// Called `broadcast` here rather than `channel`,
3454/// because the word "channel" already appears a lot in the code,
3455/// which would make it hard to grep for it.
3456///
3457/// After creation, the chat contains no recipients and is in _unpromoted_ state;
3458/// see [`create_group`] for more information on the unpromoted state.
3459///
3460/// Returns the created chat's id.
3461pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
3462    let grpid = create_id();
3463    create_broadcast_ex(context, Sync, grpid, chat_name).await
3464}
3465
3466pub(crate) async fn create_broadcast_ex(
3467    context: &Context,
3468    sync: sync::Sync,
3469    grpid: String,
3470    chat_name: String,
3471) -> Result<ChatId> {
3472    let row_id = {
3473        let chat_name = &chat_name;
3474        let grpid = &grpid;
3475        let trans_fn = |t: &mut rusqlite::Transaction| {
3476            let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
3477            ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
3478            if cnt == 1 {
3479                return Ok(t.query_row(
3480                    "SELECT id FROM chats WHERE grpid=? AND type=?",
3481                    (grpid, Chattype::OutBroadcast),
3482                    |row| {
3483                        let id: isize = row.get(0)?;
3484                        Ok(id)
3485                    },
3486                )?);
3487            }
3488            t.execute(
3489                "INSERT INTO chats \
3490                (type, name, grpid, param, created_timestamp) \
3491                VALUES(?, ?, ?, \'U=1\', ?);",
3492                (
3493                    Chattype::OutBroadcast,
3494                    &chat_name,
3495                    &grpid,
3496                    create_smeared_timestamp(context),
3497                ),
3498            )?;
3499            Ok(t.last_insert_rowid().try_into()?)
3500        };
3501        context.sql.transaction(trans_fn).await?
3502    };
3503    let chat_id = ChatId::new(u32::try_from(row_id)?);
3504
3505    context.emit_msgs_changed_without_ids();
3506    chatlist_events::emit_chatlist_changed(context);
3507
3508    if sync.into() {
3509        let id = SyncId::Grpid(grpid);
3510        let action = SyncAction::CreateBroadcast(chat_name);
3511        self::sync(context, id, action).await.log_err(context).ok();
3512    }
3513
3514    Ok(chat_id)
3515}
3516
3517/// Set chat contacts in the `chats_contacts` table.
3518pub(crate) async fn update_chat_contacts_table(
3519    context: &Context,
3520    timestamp: i64,
3521    id: ChatId,
3522    contacts: &HashSet<ContactId>,
3523) -> Result<()> {
3524    context
3525        .sql
3526        .transaction(move |transaction| {
3527            // Bump `remove_timestamp` to at least `now`
3528            // even for members from `contacts`.
3529            // We add members from `contacts` back below.
3530            transaction.execute(
3531                "UPDATE chats_contacts
3532                 SET remove_timestamp=MAX(add_timestamp+1, ?)
3533                 WHERE chat_id=?",
3534                (timestamp, id),
3535            )?;
3536
3537            if !contacts.is_empty() {
3538                let mut statement = transaction.prepare(
3539                    "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
3540                     VALUES                     (?1,      ?2,         ?3)
3541                     ON CONFLICT (chat_id, contact_id)
3542                     DO UPDATE SET add_timestamp=remove_timestamp",
3543                )?;
3544
3545                for contact_id in contacts {
3546                    // We bumped `add_timestamp` for existing rows above,
3547                    // so on conflict it is enough to set `add_timestamp = remove_timestamp`
3548                    // and this guarantees that `add_timestamp` is no less than `timestamp`.
3549                    statement.execute((id, contact_id, timestamp))?;
3550                }
3551            }
3552            Ok(())
3553        })
3554        .await?;
3555    Ok(())
3556}
3557
3558/// Adds contacts to the `chats_contacts` table.
3559pub(crate) async fn add_to_chat_contacts_table(
3560    context: &Context,
3561    timestamp: i64,
3562    chat_id: ChatId,
3563    contact_ids: &[ContactId],
3564) -> Result<()> {
3565    context
3566        .sql
3567        .transaction(move |transaction| {
3568            let mut add_statement = transaction.prepare(
3569                "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3)
3570                 ON CONFLICT (chat_id, contact_id)
3571                 DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
3572            )?;
3573
3574            for contact_id in contact_ids {
3575                add_statement.execute((chat_id, contact_id, timestamp))?;
3576            }
3577            Ok(())
3578        })
3579        .await?;
3580
3581    Ok(())
3582}
3583
3584/// Removes a contact from the chat
3585/// by updating the `remove_timestamp`.
3586pub(crate) async fn remove_from_chat_contacts_table(
3587    context: &Context,
3588    chat_id: ChatId,
3589    contact_id: ContactId,
3590) -> Result<()> {
3591    let now = time();
3592    context
3593        .sql
3594        .execute(
3595            "UPDATE chats_contacts
3596             SET remove_timestamp=MAX(add_timestamp+1, ?)
3597             WHERE chat_id=? AND contact_id=?",
3598            (now, chat_id, contact_id),
3599        )
3600        .await?;
3601    Ok(())
3602}
3603
3604/// Adds a contact to the chat.
3605/// If the group is promoted, also sends out a system message to all group members
3606pub async fn add_contact_to_chat(
3607    context: &Context,
3608    chat_id: ChatId,
3609    contact_id: ContactId,
3610) -> Result<()> {
3611    add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?;
3612    Ok(())
3613}
3614
3615pub(crate) async fn add_contact_to_chat_ex(
3616    context: &Context,
3617    mut sync: sync::Sync,
3618    chat_id: ChatId,
3619    contact_id: ContactId,
3620    from_handshake: bool,
3621) -> Result<bool> {
3622    ensure!(!chat_id.is_special(), "can not add member to special chats");
3623    let contact = Contact::get_by_id(context, contact_id).await?;
3624    let mut msg = Message::new(Viewtype::default());
3625
3626    chat_id.reset_gossiped_timestamp(context).await?;
3627
3628    // this also makes sure, no contacts are added to special or normal chats
3629    let mut chat = Chat::load_from_db(context, chat_id).await?;
3630    ensure!(
3631        chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
3632        "{chat_id} is not a group/broadcast where one can add members"
3633    );
3634    ensure!(
3635        Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
3636        "invalid contact_id {contact_id} for adding to group"
3637    );
3638    ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
3639    ensure!(
3640        chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
3641        "Cannot add SELF to broadcast channel."
3642    );
3643    ensure!(
3644        chat.is_encrypted(context).await? == contact.is_key_contact(),
3645        "Only key-contacts can be added to encrypted chats"
3646    );
3647
3648    if !chat.is_self_in_chat(context).await? {
3649        context.emit_event(EventType::ErrorSelfNotInGroup(
3650            "Cannot add contact to group; self not in group.".into(),
3651        ));
3652        bail!("can not add contact because the account is not part of the group/broadcast");
3653    }
3654
3655    let sync_qr_code_tokens;
3656    if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
3657        chat.param
3658            .remove(Param::Unpromoted)
3659            .set_i64(Param::GroupNameTimestamp, smeared_time(context));
3660        chat.update_param(context).await?;
3661        sync_qr_code_tokens = true;
3662    } else {
3663        sync_qr_code_tokens = false;
3664    }
3665
3666    if context.is_self_addr(contact.get_addr()).await? {
3667        // ourself is added using ContactId::SELF, do not add this address explicitly.
3668        // if SELF is not in the group, members cannot be added at all.
3669        warn!(
3670            context,
3671            "Invalid attempt to add self e-mail address to group."
3672        );
3673        return Ok(false);
3674    }
3675
3676    if is_contact_in_chat(context, chat_id, contact_id).await? {
3677        if !from_handshake {
3678            return Ok(true);
3679        }
3680    } else {
3681        // else continue and send status mail
3682        if is_contact_in_chat(context, chat_id, contact_id).await? {
3683            return Ok(false);
3684        }
3685        add_to_chat_contacts_table(context, time(), chat_id, &[contact_id]).await?;
3686    }
3687    if chat.typ == Chattype::Group && chat.is_promoted() {
3688        msg.viewtype = Viewtype::Text;
3689
3690        let contact_addr = contact.get_addr().to_lowercase();
3691        msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await;
3692        msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
3693        msg.param.set(Param::Arg, contact_addr);
3694        msg.param.set_int(Param::Arg2, from_handshake.into());
3695        msg.param
3696            .set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
3697        send_msg(context, chat_id, &mut msg).await?;
3698
3699        sync = Nosync;
3700        // TODO: Remove this compat code needed because Core <= v1.143:
3701        // - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also send
3702        //   them when the group is promoted.
3703        // - doesn't sync QR code tokens for unpromoted groups and the group might be created before
3704        //   an upgrade.
3705        if sync_qr_code_tokens
3706            && context
3707                .sync_qr_code_tokens(Some(chat.grpid.as_str()))
3708                .await
3709                .log_err(context)
3710                .is_ok()
3711        {
3712            context.scheduler.interrupt_inbox().await;
3713        }
3714    }
3715    context.emit_event(EventType::ChatModified(chat_id));
3716    if sync.into() {
3717        chat.sync_contacts(context).await.log_err(context).ok();
3718    }
3719    Ok(true)
3720}
3721
3722/// Returns true if an avatar should be attached in the given chat.
3723///
3724/// This function does not check if the avatar is set.
3725/// If avatar is not set and this function returns `true`,
3726/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar.
3727pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
3728    let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
3729    let needs_attach = context
3730        .sql
3731        .query_map(
3732            "SELECT c.selfavatar_sent
3733             FROM chats_contacts cc
3734             LEFT JOIN contacts c ON c.id=cc.contact_id
3735             WHERE cc.chat_id=? AND cc.contact_id!=? AND cc.add_timestamp >= cc.remove_timestamp",
3736            (chat_id, ContactId::SELF),
3737            |row| Ok(row.get::<_, i64>(0)),
3738            |rows| {
3739                let mut needs_attach = false;
3740                for row in rows {
3741                    let row = row?;
3742                    let selfavatar_sent = row?;
3743                    if selfavatar_sent < timestamp_some_days_ago {
3744                        needs_attach = true;
3745                    }
3746                }
3747                Ok(needs_attach)
3748            },
3749        )
3750        .await?;
3751    Ok(needs_attach)
3752}
3753
3754/// Chat mute duration.
3755#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
3756pub enum MuteDuration {
3757    /// Chat is not muted.
3758    NotMuted,
3759
3760    /// Chat is muted until the user unmutes the chat.
3761    Forever,
3762
3763    /// Chat is muted for a limited period of time.
3764    Until(std::time::SystemTime),
3765}
3766
3767impl rusqlite::types::ToSql for MuteDuration {
3768    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
3769        let duration: i64 = match &self {
3770            MuteDuration::NotMuted => 0,
3771            MuteDuration::Forever => -1,
3772            MuteDuration::Until(when) => {
3773                let duration = when
3774                    .duration_since(SystemTime::UNIX_EPOCH)
3775                    .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
3776                i64::try_from(duration.as_secs())
3777                    .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
3778            }
3779        };
3780        let val = rusqlite::types::Value::Integer(duration);
3781        let out = rusqlite::types::ToSqlOutput::Owned(val);
3782        Ok(out)
3783    }
3784}
3785
3786impl rusqlite::types::FromSql for MuteDuration {
3787    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
3788        // Negative values other than -1 should not be in the
3789        // database.  If found they'll be NotMuted.
3790        match i64::column_result(value)? {
3791            0 => Ok(MuteDuration::NotMuted),
3792            -1 => Ok(MuteDuration::Forever),
3793            n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
3794                Some(t) => Ok(MuteDuration::Until(t)),
3795                None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
3796            },
3797            _ => Ok(MuteDuration::NotMuted),
3798        }
3799    }
3800}
3801
3802/// Mutes the chat for a given duration or unmutes it.
3803pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<()> {
3804    set_muted_ex(context, Sync, chat_id, duration).await
3805}
3806
3807pub(crate) async fn set_muted_ex(
3808    context: &Context,
3809    sync: sync::Sync,
3810    chat_id: ChatId,
3811    duration: MuteDuration,
3812) -> Result<()> {
3813    ensure!(!chat_id.is_special(), "Invalid chat ID");
3814    context
3815        .sql
3816        .execute(
3817            "UPDATE chats SET muted_until=? WHERE id=?;",
3818            (duration, chat_id),
3819        )
3820        .await
3821        .context(format!("Failed to set mute duration for {chat_id}"))?;
3822    context.emit_event(EventType::ChatModified(chat_id));
3823    chatlist_events::emit_chatlist_item_changed(context, chat_id);
3824    if sync.into() {
3825        let chat = Chat::load_from_db(context, chat_id).await?;
3826        chat.sync(context, SyncAction::SetMuted(duration))
3827            .await
3828            .log_err(context)
3829            .ok();
3830    }
3831    Ok(())
3832}
3833
3834/// Removes contact from the chat.
3835pub async fn remove_contact_from_chat(
3836    context: &Context,
3837    chat_id: ChatId,
3838    contact_id: ContactId,
3839) -> Result<()> {
3840    ensure!(
3841        !chat_id.is_special(),
3842        "bad chat_id, can not be special chat: {chat_id}"
3843    );
3844    ensure!(
3845        !contact_id.is_special() || contact_id == ContactId::SELF,
3846        "Cannot remove special contact"
3847    );
3848
3849    let chat = Chat::load_from_db(context, chat_id).await?;
3850    if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
3851        if !chat.is_self_in_chat(context).await? {
3852            let err_msg = format!(
3853                "Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
3854            );
3855            context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
3856            bail!("{err_msg}");
3857        } else {
3858            let mut sync = Nosync;
3859
3860            if chat.is_promoted() {
3861                remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
3862            } else {
3863                context
3864                    .sql
3865                    .execute(
3866                        "DELETE FROM chats_contacts
3867                         WHERE chat_id=? AND contact_id=?",
3868                        (chat_id, contact_id),
3869                    )
3870                    .await?;
3871            }
3872
3873            // We do not return an error if the contact does not exist in the database.
3874            // This allows to delete dangling references to deleted contacts
3875            // in case of the database becoming inconsistent due to a bug.
3876            if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
3877                if chat.typ == Chattype::Group && chat.is_promoted() {
3878                    let addr = contact.get_addr();
3879
3880                    let res = send_member_removal_msg(context, &chat, contact_id, addr).await;
3881
3882                    if contact_id == ContactId::SELF {
3883                        res?;
3884                    } else if let Err(e) = res {
3885                        warn!(
3886                            context,
3887                            "remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}."
3888                        );
3889                    }
3890                } else {
3891                    sync = Sync;
3892                }
3893            }
3894            context.emit_event(EventType::ChatModified(chat_id));
3895            if sync.into() {
3896                chat.sync_contacts(context).await.log_err(context).ok();
3897            }
3898        }
3899    } else if chat.typ == Chattype::InBroadcast && contact_id == ContactId::SELF {
3900        // For incoming broadcast channels, it's not possible to remove members,
3901        // but it's possible to leave:
3902        let self_addr = context.get_primary_self_addr().await?;
3903        send_member_removal_msg(context, &chat, contact_id, &self_addr).await?;
3904    } else {
3905        bail!("Cannot remove members from non-group chats.");
3906    }
3907
3908    Ok(())
3909}
3910
3911async fn send_member_removal_msg(
3912    context: &Context,
3913    chat: &Chat,
3914    contact_id: ContactId,
3915    addr: &str,
3916) -> Result<MsgId> {
3917    let mut msg = Message::new(Viewtype::Text);
3918
3919    if contact_id == ContactId::SELF {
3920        if chat.typ == Chattype::InBroadcast {
3921            msg.text = stock_str::msg_you_left_broadcast(context).await;
3922        } else {
3923            msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
3924        }
3925    } else {
3926        msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
3927    }
3928
3929    msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
3930    msg.param.set(Param::Arg, addr.to_lowercase());
3931    msg.param
3932        .set(Param::ContactAddedRemoved, contact_id.to_u32());
3933
3934    send_msg(context, chat.id, &mut msg).await
3935}
3936
3937/// Sets group or mailing list chat name.
3938pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
3939    rename_ex(context, Sync, chat_id, new_name).await
3940}
3941
3942async fn rename_ex(
3943    context: &Context,
3944    mut sync: sync::Sync,
3945    chat_id: ChatId,
3946    new_name: &str,
3947) -> Result<()> {
3948    let new_name = sanitize_single_line(new_name);
3949    /* the function only sets the names of group chats; normal chats get their names from the contacts */
3950    let mut success = false;
3951
3952    ensure!(!new_name.is_empty(), "Invalid name");
3953    ensure!(!chat_id.is_special(), "Invalid chat ID");
3954
3955    let chat = Chat::load_from_db(context, chat_id).await?;
3956    let mut msg = Message::new(Viewtype::default());
3957
3958    if chat.typ == Chattype::Group
3959        || chat.typ == Chattype::Mailinglist
3960        || chat.typ == Chattype::OutBroadcast
3961    {
3962        if chat.name == new_name {
3963            success = true;
3964        } else if !chat.is_self_in_chat(context).await? {
3965            context.emit_event(EventType::ErrorSelfNotInGroup(
3966                "Cannot set chat name; self not in group".into(),
3967            ));
3968        } else {
3969            context
3970                .sql
3971                .execute(
3972                    "UPDATE chats SET name=? WHERE id=?;",
3973                    (new_name.to_string(), chat_id),
3974                )
3975                .await?;
3976            if chat.is_promoted()
3977                && !chat.is_mailing_list()
3978                && sanitize_single_line(&chat.name) != new_name
3979            {
3980                msg.viewtype = Viewtype::Text;
3981                msg.text =
3982                    stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
3983                msg.param.set_cmd(SystemMessage::GroupNameChanged);
3984                if !chat.name.is_empty() {
3985                    msg.param.set(Param::Arg, &chat.name);
3986                }
3987                msg.id = send_msg(context, chat_id, &mut msg).await?;
3988                context.emit_msgs_changed(chat_id, msg.id);
3989                sync = Nosync;
3990            }
3991            context.emit_event(EventType::ChatModified(chat_id));
3992            chatlist_events::emit_chatlist_item_changed(context, chat_id);
3993            success = true;
3994        }
3995    }
3996
3997    if !success {
3998        bail!("Failed to set name");
3999    }
4000    if sync.into() && chat.name != new_name {
4001        let sync_name = new_name.to_string();
4002        chat.sync(context, SyncAction::Rename(sync_name))
4003            .await
4004            .log_err(context)
4005            .ok();
4006    }
4007    Ok(())
4008}
4009
4010/// Sets a new profile image for the chat.
4011///
4012/// The profile image can only be set when you are a member of the
4013/// chat.  To remove the profile image pass an empty string for the
4014/// `new_image` parameter.
4015pub async fn set_chat_profile_image(
4016    context: &Context,
4017    chat_id: ChatId,
4018    new_image: &str, // XXX use PathBuf
4019) -> Result<()> {
4020    ensure!(!chat_id.is_special(), "Invalid chat ID");
4021    let mut chat = Chat::load_from_db(context, chat_id).await?;
4022    ensure!(
4023        chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
4024        "Can only set profile image for groups / broadcasts"
4025    );
4026    ensure!(
4027        !chat.grpid.is_empty(),
4028        "Cannot set profile image for ad hoc groups"
4029    );
4030    /* we should respect this - whatever we send to the group, it gets discarded anyway! */
4031    if !chat.is_self_in_chat(context).await? {
4032        context.emit_event(EventType::ErrorSelfNotInGroup(
4033            "Cannot set chat profile image; self not in group.".into(),
4034        ));
4035        bail!("Failed to set profile image");
4036    }
4037    let mut msg = Message::new(Viewtype::Text);
4038    msg.param
4039        .set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
4040    if new_image.is_empty() {
4041        chat.param.remove(Param::ProfileImage);
4042        msg.param.remove(Param::Arg);
4043        msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await;
4044    } else {
4045        let mut image_blob = BlobObject::create_and_deduplicate(
4046            context,
4047            Path::new(new_image),
4048            Path::new(new_image),
4049        )?;
4050        image_blob.recode_to_avatar_size(context).await?;
4051        chat.param.set(Param::ProfileImage, image_blob.as_name());
4052        msg.param.set(Param::Arg, image_blob.as_name());
4053        msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
4054    }
4055    chat.update_param(context).await?;
4056    if chat.is_promoted() {
4057        msg.id = send_msg(context, chat_id, &mut msg).await?;
4058        context.emit_msgs_changed(chat_id, msg.id);
4059    }
4060    context.emit_event(EventType::ChatModified(chat_id));
4061    chatlist_events::emit_chatlist_item_changed(context, chat_id);
4062    Ok(())
4063}
4064
4065/// Forwards multiple messages to a chat.
4066pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
4067    ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
4068    ensure!(!chat_id.is_special(), "can not forward to special chat");
4069
4070    let mut created_msgs: Vec<MsgId> = Vec::new();
4071    let mut curr_timestamp: i64;
4072
4073    chat_id
4074        .unarchive_if_not_muted(context, MessageState::Undefined)
4075        .await?;
4076    let mut chat = Chat::load_from_db(context, chat_id).await?;
4077    if let Some(reason) = chat.why_cant_send(context).await? {
4078        bail!("cannot send to {chat_id}: {reason}");
4079    }
4080    curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
4081    let mut msgs = Vec::with_capacity(msg_ids.len());
4082    for id in msg_ids {
4083        let ts: i64 = context
4084            .sql
4085            .query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
4086            .await?
4087            .with_context(|| format!("No message {id}"))?;
4088        msgs.push((ts, *id));
4089    }
4090    msgs.sort_unstable();
4091    for (_, id) in msgs {
4092        let src_msg_id: MsgId = id;
4093        let mut msg = Message::load_from_db(context, src_msg_id).await?;
4094        if msg.state == MessageState::OutDraft {
4095            bail!("cannot forward drafts.");
4096        }
4097
4098        if msg.get_viewtype() != Viewtype::Sticker {
4099            msg.param
4100                .set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
4101        }
4102
4103        if msg.get_viewtype() == Viewtype::Call {
4104            msg.viewtype = Viewtype::Text;
4105        }
4106
4107        msg.param.remove(Param::GuaranteeE2ee);
4108        msg.param.remove(Param::ForcePlaintext);
4109        msg.param.remove(Param::Cmd);
4110        msg.param.remove(Param::OverrideSenderDisplayname);
4111        msg.param.remove(Param::WebxdcDocument);
4112        msg.param.remove(Param::WebxdcDocumentTimestamp);
4113        msg.param.remove(Param::WebxdcSummary);
4114        msg.param.remove(Param::WebxdcSummaryTimestamp);
4115        msg.param.remove(Param::IsEdited);
4116        msg.param.remove(Param::WebrtcRoom);
4117        msg.param.remove(Param::WebrtcAccepted);
4118        msg.in_reply_to = None;
4119
4120        // do not leak data as group names; a default subject is generated by mimefactory
4121        msg.subject = "".to_string();
4122
4123        msg.state = MessageState::OutPending;
4124        msg.rfc724_mid = create_outgoing_rfc724_mid();
4125        msg.timestamp_sort = curr_timestamp;
4126        chat.prepare_msg_raw(context, &mut msg, None).await?;
4127
4128        curr_timestamp += 1;
4129        if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
4130            context.scheduler.interrupt_smtp().await;
4131        }
4132        created_msgs.push(msg.id);
4133    }
4134    for msg_id in created_msgs {
4135        context.emit_msgs_changed(chat_id, msg_id);
4136    }
4137    Ok(())
4138}
4139
4140/// Save a copy of the message in "Saved Messages"
4141/// and send a sync messages so that other devices save the message as well, unless deleted there.
4142pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
4143    let mut msgs = Vec::with_capacity(msg_ids.len());
4144    for id in msg_ids {
4145        let ts: i64 = context
4146            .sql
4147            .query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
4148            .await?
4149            .with_context(|| format!("No message {id}"))?;
4150        msgs.push((ts, *id));
4151    }
4152    msgs.sort_unstable();
4153    for (_, src_msg_id) in msgs {
4154        let dest_rfc724_mid = create_outgoing_rfc724_mid();
4155        let src_rfc724_mid = save_copy_in_self_talk(context, src_msg_id, &dest_rfc724_mid).await?;
4156        context
4157            .add_sync_item(SyncData::SaveMessage {
4158                src: src_rfc724_mid,
4159                dest: dest_rfc724_mid,
4160            })
4161            .await?;
4162    }
4163    context.scheduler.interrupt_inbox().await;
4164    Ok(())
4165}
4166
4167/// Saves a copy of the given message in "Saved Messages" using the given RFC724 id.
4168/// To allow UIs to have a "show in context" button,
4169/// the copy contains a reference to the original message
4170/// as well as to the original chat in case the original message gets deleted.
4171/// Returns data needed to add a `SaveMessage` sync item.
4172pub(crate) async fn save_copy_in_self_talk(
4173    context: &Context,
4174    src_msg_id: MsgId,
4175    dest_rfc724_mid: &String,
4176) -> Result<String> {
4177    let dest_chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
4178    let mut msg = Message::load_from_db(context, src_msg_id).await?;
4179    msg.param.remove(Param::Cmd);
4180    msg.param.remove(Param::WebxdcDocument);
4181    msg.param.remove(Param::WebxdcDocumentTimestamp);
4182    msg.param.remove(Param::WebxdcSummary);
4183    msg.param.remove(Param::WebxdcSummaryTimestamp);
4184
4185    if !msg.original_msg_id.is_unset() {
4186        bail!("message already saved.");
4187    }
4188
4189    let copy_fields = "from_id, to_id, timestamp_rcvd, type, txt,
4190                       mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
4191    let row_id = context
4192        .sql
4193        .insert(
4194            &format!(
4195                "INSERT INTO msgs ({copy_fields},
4196                                   timestamp_sent,
4197                                   chat_id, rfc724_mid, state, timestamp, param, starred)
4198                 SELECT            {copy_fields},
4199                                   -- Outgoing messages on originating device
4200                                   -- have timestamp_sent == 0.
4201                                   -- We copy sort timestamp instead
4202                                   -- so UIs display the same timestamp
4203                                   -- for saved and original message.
4204                                   IIF(timestamp_sent == 0, timestamp, timestamp_sent),
4205                                   ?, ?, ?, ?, ?, ?
4206                 FROM msgs WHERE id=?;"
4207            ),
4208            (
4209                dest_chat_id,
4210                dest_rfc724_mid,
4211                if msg.from_id == ContactId::SELF {
4212                    MessageState::OutDelivered
4213                } else {
4214                    MessageState::InSeen
4215                },
4216                create_smeared_timestamp(context),
4217                msg.param.to_string(),
4218                src_msg_id,
4219                src_msg_id,
4220            ),
4221        )
4222        .await?;
4223    let dest_msg_id = MsgId::new(row_id.try_into()?);
4224
4225    context.emit_msgs_changed(msg.chat_id, src_msg_id);
4226    context.emit_msgs_changed(dest_chat_id, dest_msg_id);
4227    chatlist_events::emit_chatlist_changed(context);
4228    chatlist_events::emit_chatlist_item_changed(context, dest_chat_id);
4229
4230    Ok(msg.rfc724_mid)
4231}
4232
4233/// Resends given messages with the same Message-ID.
4234///
4235/// This is primarily intended to make existing webxdcs available to new chat members.
4236pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
4237    let mut msgs: Vec<Message> = Vec::new();
4238    for msg_id in msg_ids {
4239        let msg = Message::load_from_db(context, *msg_id).await?;
4240        ensure!(
4241            msg.from_id == ContactId::SELF,
4242            "can resend only own messages"
4243        );
4244        ensure!(!msg.is_info(), "cannot resend info messages");
4245        msgs.push(msg)
4246    }
4247
4248    for mut msg in msgs {
4249        match msg.get_state() {
4250            // `get_state()` may return an outdated `OutPending`, so update anyway.
4251            MessageState::OutPending
4252            | MessageState::OutFailed
4253            | MessageState::OutDelivered
4254            | MessageState::OutMdnRcvd => {
4255                message::update_msg_state(context, msg.id, MessageState::OutPending).await?
4256            }
4257            msg_state => bail!("Unexpected message state {msg_state}"),
4258        }
4259        msg.timestamp_sort = create_smeared_timestamp(context);
4260        if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
4261            continue;
4262        }
4263
4264        // Emit the event only after `create_send_msg_jobs`
4265        // because `create_send_msg_jobs` may change the message
4266        // encryption status and call `msg.update_param`.
4267        context.emit_event(EventType::MsgsChanged {
4268            chat_id: msg.chat_id,
4269            msg_id: msg.id,
4270        });
4271        // note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
4272        chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
4273
4274        if msg.viewtype == Viewtype::Webxdc {
4275            let conn_fn = |conn: &mut rusqlite::Connection| {
4276                let range = conn.query_row(
4277                    "SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \
4278                     FROM msgs_status_updates WHERE msg_id=?",
4279                    (msg.id,),
4280                    |row| {
4281                        let min_id: StatusUpdateSerial = row.get(0)?;
4282                        let max_id: StatusUpdateSerial = row.get(1)?;
4283                        Ok((min_id, max_id))
4284                    },
4285                )?;
4286                if range.0 > range.1 {
4287                    return Ok(());
4288                };
4289                // `first_serial` must be decreased, otherwise if `Context::flush_status_updates()`
4290                // runs in parallel, it would miss the race and instead of resending just remove the
4291                // updates thinking that they have been already sent.
4292                conn.execute(
4293                    "INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) \
4294                     VALUES(?, ?, ?, '') \
4295                     ON CONFLICT(msg_id) \
4296                     DO UPDATE SET first_serial=min(first_serial - 1, excluded.first_serial)",
4297                    (msg.id, range.0, range.1),
4298                )?;
4299                Ok(())
4300            };
4301            context.sql.call_write(conn_fn).await?;
4302        }
4303        context.scheduler.interrupt_smtp().await;
4304    }
4305    Ok(())
4306}
4307
4308pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
4309    if context.sql.is_open().await {
4310        // no database, no chats - this is no error (needed eg. for information)
4311        let count = context
4312            .sql
4313            .count("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;", ())
4314            .await?;
4315        Ok(count)
4316    } else {
4317        Ok(0)
4318    }
4319}
4320
4321/// Returns a tuple of `(chatid, blocked)`.
4322pub(crate) async fn get_chat_id_by_grpid(
4323    context: &Context,
4324    grpid: &str,
4325) -> Result<Option<(ChatId, Blocked)>> {
4326    context
4327        .sql
4328        .query_row_optional(
4329            "SELECT id, blocked FROM chats WHERE grpid=?;",
4330            (grpid,),
4331            |row| {
4332                let chat_id = row.get::<_, ChatId>(0)?;
4333
4334                let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
4335                Ok((chat_id, b))
4336            },
4337        )
4338        .await
4339}
4340
4341/// Adds a message to device chat.
4342///
4343/// Optional `label` can be provided to ensure that message is added only once.
4344/// If `important` is true, a notification will be sent.
4345pub async fn add_device_msg_with_importance(
4346    context: &Context,
4347    label: Option<&str>,
4348    msg: Option<&mut Message>,
4349    important: bool,
4350) -> Result<MsgId> {
4351    ensure!(
4352        label.is_some() || msg.is_some(),
4353        "device-messages need label, msg or both"
4354    );
4355    let mut chat_id = ChatId::new(0);
4356    let mut msg_id = MsgId::new_unset();
4357
4358    if let Some(label) = label {
4359        if was_device_msg_ever_added(context, label).await? {
4360            info!(context, "Device-message {label} already added.");
4361            return Ok(msg_id);
4362        }
4363    }
4364
4365    if let Some(msg) = msg {
4366        chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
4367
4368        let rfc724_mid = create_outgoing_rfc724_mid();
4369        let timestamp_sent = create_smeared_timestamp(context);
4370
4371        // makes sure, the added message is the last one,
4372        // even if the date is wrong (useful esp. when warning about bad dates)
4373        msg.timestamp_sort = timestamp_sent;
4374        if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
4375            if msg.timestamp_sort <= last_msg_time {
4376                msg.timestamp_sort = last_msg_time + 1;
4377            }
4378        }
4379        prepare_msg_blob(context, msg).await?;
4380        let state = MessageState::InFresh;
4381        let row_id = context
4382            .sql
4383            .insert(
4384                "INSERT INTO msgs (
4385            chat_id,
4386            from_id,
4387            to_id,
4388            timestamp,
4389            timestamp_sent,
4390            timestamp_rcvd,
4391            type,state,
4392            txt,
4393            txt_normalized,
4394            param,
4395            rfc724_mid)
4396            VALUES (?,?,?,?,?,?,?,?,?,?,?,?);",
4397                (
4398                    chat_id,
4399                    ContactId::DEVICE,
4400                    ContactId::SELF,
4401                    msg.timestamp_sort,
4402                    timestamp_sent,
4403                    timestamp_sent, // timestamp_sent equals timestamp_rcvd
4404                    msg.viewtype,
4405                    state,
4406                    &msg.text,
4407                    message::normalize_text(&msg.text),
4408                    msg.param.to_string(),
4409                    rfc724_mid,
4410                ),
4411            )
4412            .await?;
4413        context.new_msgs_notify.notify_one();
4414
4415        msg_id = MsgId::new(u32::try_from(row_id)?);
4416        if !msg.hidden {
4417            chat_id.unarchive_if_not_muted(context, state).await?;
4418        }
4419    }
4420
4421    if let Some(label) = label {
4422        context
4423            .sql
4424            .execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
4425            .await?;
4426    }
4427
4428    if !msg_id.is_unset() {
4429        chat_id.emit_msg_event(context, msg_id, important);
4430    }
4431
4432    Ok(msg_id)
4433}
4434
4435/// Adds a message to device chat.
4436pub async fn add_device_msg(
4437    context: &Context,
4438    label: Option<&str>,
4439    msg: Option<&mut Message>,
4440) -> Result<MsgId> {
4441    add_device_msg_with_importance(context, label, msg, false).await
4442}
4443
4444/// Returns true if device message with a given label was ever added to the device chat.
4445pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool> {
4446    ensure!(!label.is_empty(), "empty label");
4447    let exists = context
4448        .sql
4449        .exists(
4450            "SELECT COUNT(label) FROM devmsglabels WHERE label=?",
4451            (label,),
4452        )
4453        .await?;
4454
4455    Ok(exists)
4456}
4457
4458// needed on device-switches during export/import;
4459// - deletion in `msgs` with `ContactId::DEVICE` makes sure,
4460//   no wrong information are shown in the device chat
4461// - deletion in `devmsglabels` makes sure,
4462//   deleted messages are reset and useful messages can be added again
4463// - we reset the config-option `QuotaExceeding`
4464//   that is used as a helper to drive the corresponding device message.
4465pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
4466    context
4467        .sql
4468        .execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
4469        .await?;
4470    context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
4471
4472    // Insert labels for welcome messages to avoid them being re-added on reconfiguration.
4473    context
4474        .sql
4475        .execute(
4476            r#"INSERT INTO devmsglabels (label) VALUES ("core-welcome-image"), ("core-welcome")"#,
4477            (),
4478        )
4479        .await?;
4480    context
4481        .set_config_internal(Config::QuotaExceeding, None)
4482        .await?;
4483    Ok(())
4484}
4485
4486/// Adds an informational message to chat.
4487///
4488/// For example, it can be a message showing that a member was added to a group.
4489/// Doesn't fail if the chat doesn't exist.
4490#[expect(clippy::too_many_arguments)]
4491pub(crate) async fn add_info_msg_with_cmd(
4492    context: &Context,
4493    chat_id: ChatId,
4494    text: &str,
4495    cmd: SystemMessage,
4496    timestamp_sort: i64,
4497    // Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
4498    timestamp_sent_rcvd: Option<i64>,
4499    parent: Option<&Message>,
4500    from_id: Option<ContactId>,
4501    added_removed_id: Option<ContactId>,
4502) -> Result<MsgId> {
4503    let rfc724_mid = create_outgoing_rfc724_mid();
4504    let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
4505
4506    let mut param = Params::new();
4507    if cmd != SystemMessage::Unknown {
4508        param.set_cmd(cmd);
4509    }
4510    if let Some(contact_id) = added_removed_id {
4511        param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
4512    }
4513
4514    let row_id =
4515    context.sql.insert(
4516        "INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
4517        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
4518        (
4519            chat_id,
4520            from_id.unwrap_or(ContactId::INFO),
4521            ContactId::INFO,
4522            timestamp_sort,
4523            timestamp_sent_rcvd.unwrap_or(0),
4524            timestamp_sent_rcvd.unwrap_or(0),
4525            Viewtype::Text,
4526            MessageState::InNoticed,
4527            text,
4528            message::normalize_text(text),
4529            rfc724_mid,
4530            ephemeral_timer,
4531            param.to_string(),
4532            parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
4533        )
4534    ).await?;
4535    context.new_msgs_notify.notify_one();
4536
4537    let msg_id = MsgId::new(row_id.try_into()?);
4538    context.emit_msgs_changed(chat_id, msg_id);
4539
4540    Ok(msg_id)
4541}
4542
4543/// Adds info message with a given text and `timestamp` to the chat.
4544pub(crate) async fn add_info_msg(
4545    context: &Context,
4546    chat_id: ChatId,
4547    text: &str,
4548    timestamp: i64,
4549) -> Result<MsgId> {
4550    add_info_msg_with_cmd(
4551        context,
4552        chat_id,
4553        text,
4554        SystemMessage::Unknown,
4555        timestamp,
4556        None,
4557        None,
4558        None,
4559        None,
4560    )
4561    .await
4562}
4563
4564pub(crate) async fn update_msg_text_and_timestamp(
4565    context: &Context,
4566    chat_id: ChatId,
4567    msg_id: MsgId,
4568    text: &str,
4569    timestamp: i64,
4570) -> Result<()> {
4571    context
4572        .sql
4573        .execute(
4574            "UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
4575            (text, message::normalize_text(text), timestamp, msg_id),
4576        )
4577        .await?;
4578    context.emit_msgs_changed(chat_id, msg_id);
4579    Ok(())
4580}
4581
4582/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
4583async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
4584    let chat = Chat::load_from_db(context, id).await?;
4585    ensure!(
4586        !chat.is_encrypted(context).await?,
4587        "Cannot add address-contacts to encrypted chat {id}"
4588    );
4589    ensure!(
4590        chat.typ == Chattype::OutBroadcast,
4591        "{id} is not a broadcast list",
4592    );
4593    let mut contacts = HashSet::new();
4594    for addr in addrs {
4595        let contact_addr = ContactAddress::new(addr)?;
4596        let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
4597            .await?
4598            .0;
4599        contacts.insert(contact);
4600    }
4601    let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
4602    if contacts == contacts_old {
4603        return Ok(());
4604    }
4605    context
4606        .sql
4607        .transaction(move |transaction| {
4608            transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
4609
4610            // We do not care about `add_timestamp` column
4611            // because timestamps are not used for broadcast channels.
4612            let mut statement = transaction
4613                .prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
4614            for contact_id in &contacts {
4615                statement.execute((id, contact_id))?;
4616            }
4617            Ok(())
4618        })
4619        .await?;
4620    context.emit_event(EventType::ChatModified(id));
4621    Ok(())
4622}
4623
4624/// Set chat contacts by their fingerprints creating the corresponding contacts if necessary.
4625///
4626/// `fingerprint_addrs` is a list of pairs of fingerprint and address.
4627async fn set_contacts_by_fingerprints(
4628    context: &Context,
4629    id: ChatId,
4630    fingerprint_addrs: &[(String, String)],
4631) -> Result<()> {
4632    let chat = Chat::load_from_db(context, id).await?;
4633    ensure!(
4634        chat.is_encrypted(context).await?,
4635        "Cannot add key-contacts to unencrypted chat {id}"
4636    );
4637    ensure!(
4638        matches!(chat.typ, Chattype::Group | Chattype::OutBroadcast),
4639        "{id} is not a group or broadcast",
4640    );
4641    let mut contacts = HashSet::new();
4642    for (fingerprint, addr) in fingerprint_addrs {
4643        let contact = Contact::add_or_lookup_ex(context, "", addr, fingerprint, Origin::Hidden)
4644            .await?
4645            .0;
4646        contacts.insert(contact);
4647    }
4648    let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
4649    if contacts == contacts_old {
4650        return Ok(());
4651    }
4652    context
4653        .sql
4654        .transaction(move |transaction| {
4655            transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
4656
4657            // We do not care about `add_timestamp` column
4658            // because timestamps are not used for broadcast channels.
4659            let mut statement = transaction
4660                .prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
4661            for contact_id in &contacts {
4662                statement.execute((id, contact_id))?;
4663            }
4664            Ok(())
4665        })
4666        .await?;
4667    context.emit_event(EventType::ChatModified(id));
4668    Ok(())
4669}
4670
4671/// A cross-device chat id used for synchronisation.
4672#[derive(Debug, Serialize, Deserialize, PartialEq)]
4673pub(crate) enum SyncId {
4674    /// E-mail address of the contact.
4675    ContactAddr(String),
4676
4677    /// OpenPGP key fingerprint of the contact.
4678    ContactFingerprint(String),
4679
4680    Grpid(String),
4681    /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
4682    Msgids(Vec<String>),
4683
4684    /// Special id for device chat.
4685    Device,
4686}
4687
4688/// An action synchronised to other devices.
4689#[derive(Debug, Serialize, Deserialize, PartialEq)]
4690pub(crate) enum SyncAction {
4691    Block,
4692    Unblock,
4693    Accept,
4694    SetVisibility(ChatVisibility),
4695    SetMuted(MuteDuration),
4696    /// Create broadcast channel with the given name.
4697    CreateBroadcast(String),
4698    /// Create encrypted group chat with the given name.
4699    CreateGroupEncrypted(String),
4700    Rename(String),
4701    /// Set chat contacts by their addresses.
4702    SetContacts(Vec<String>),
4703    /// Set chat contacts by their fingerprints.
4704    ///
4705    /// The list is a list of pairs of fingerprint and address.
4706    SetPgpContacts(Vec<(String, String)>),
4707    Delete,
4708}
4709
4710impl Context {
4711    /// Executes [`SyncData::AlterChat`] item sent by other device.
4712    pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
4713        let chat_id = match id {
4714            SyncId::ContactAddr(addr) => {
4715                if let SyncAction::Rename(to) = action {
4716                    Contact::create_ex(self, Nosync, to, addr).await?;
4717                    return Ok(());
4718                }
4719                let addr = ContactAddress::new(addr).context("Invalid address")?;
4720                let (contact_id, _) =
4721                    Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
4722                match action {
4723                    SyncAction::Block => {
4724                        return contact::set_blocked(self, Nosync, contact_id, true).await;
4725                    }
4726                    SyncAction::Unblock => {
4727                        return contact::set_blocked(self, Nosync, contact_id, false).await;
4728                    }
4729                    _ => (),
4730                }
4731                // Use `Request` so that even if the program crashes, the user doesn't have to look
4732                // into the blocked contacts.
4733                ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
4734                    .await?
4735                    .id
4736            }
4737            SyncId::ContactFingerprint(fingerprint) => {
4738                let name = "";
4739                let addr = "";
4740                let (contact_id, _) =
4741                    Contact::add_or_lookup_ex(self, name, addr, fingerprint, Origin::Hidden)
4742                        .await?;
4743                match action {
4744                    SyncAction::Rename(to) => {
4745                        contact_id.set_name_ex(self, Nosync, to).await?;
4746                        self.emit_event(EventType::ContactsChanged(Some(contact_id)));
4747                        return Ok(());
4748                    }
4749                    SyncAction::Block => {
4750                        return contact::set_blocked(self, Nosync, contact_id, true).await;
4751                    }
4752                    SyncAction::Unblock => {
4753                        return contact::set_blocked(self, Nosync, contact_id, false).await;
4754                    }
4755                    _ => (),
4756                }
4757                ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
4758                    .await?
4759                    .id
4760            }
4761            SyncId::Grpid(grpid) => {
4762                if let SyncAction::CreateBroadcast(name) = action {
4763                    create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
4764                    return Ok(());
4765                } else if let SyncAction::CreateGroupEncrypted(name) = action {
4766                    create_group_ex(self, Nosync, grpid.clone(), name).await?;
4767                    return Ok(());
4768                }
4769                get_chat_id_by_grpid(self, grpid)
4770                    .await?
4771                    .with_context(|| format!("No chat for grpid '{grpid}'"))?
4772                    .0
4773            }
4774            SyncId::Msgids(msgids) => {
4775                let msg = message::get_by_rfc724_mids(self, msgids)
4776                    .await?
4777                    .with_context(|| format!("No message found for Message-IDs {msgids:?}"))?;
4778                ChatId::lookup_by_message(&msg)
4779                    .with_context(|| format!("No chat found for Message-IDs {msgids:?}"))?
4780            }
4781            SyncId::Device => ChatId::get_for_contact(self, ContactId::DEVICE).await?,
4782        };
4783        match action {
4784            SyncAction::Block => chat_id.block_ex(self, Nosync).await,
4785            SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
4786            SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
4787            SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
4788            SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
4789            SyncAction::CreateBroadcast(_) | SyncAction::CreateGroupEncrypted(..) => {
4790                Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
4791            }
4792            SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
4793            SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
4794            SyncAction::SetPgpContacts(fingerprint_addrs) => {
4795                set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
4796            }
4797            SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
4798        }
4799    }
4800
4801    /// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
4802    /// archived chats could decrease. In general we don't want to make an extra db query to know if
4803    /// a noticed chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
4804    /// is ok.
4805    pub(crate) fn on_archived_chats_maybe_noticed(&self) {
4806        self.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
4807    }
4808}
4809
4810#[cfg(test)]
4811mod chat_tests;