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