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