mirror of
https://github.com/chatmail/core.git
synced 2026-05-02 21:06:31 +03:00
feat: Show broadcast channels in their own, proper "Channel" chat (#6901)
Part of #6884 ---- - [x] Add new chat type `InBroadcastChannel` and `OutBroadcastChannel` for incoming / outgoing channels, where the former is similar to a `Mailinglist` and the latter is similar to a `Broadcast` (which is removed) - Consideration for naming: `InChannel`/`OutChannel` (without "broadcast") would be shorter, but less greppable because we already have a lot of occurences of `channel` in the code. Consistently calling them `BcChannel`/`bc_channel` in the code would be both short and greppable, but a bit arcane when reading it at first. Opinions are welcome; if I hear none, I'll keep with `BroadcastChannel`. - [x] api: Add create_broadcast_channel(), deprecate create_broadcast_list() (or `create_channel()` / `create_bc_channel()` if we decide to switch) - Adjust code comments to match the new behavior. - [x] Ask Desktop developers what they use `is_broadcast` field for, and whether it should be true for both outgoing & incoming channels (or look it up myself) - I added `is_out_broadcast_channel`, and deprecated `is_broadcast`, for now - [x] When the user changes the broadcast channel name, immediately show this change on receiving devices - [x] Allow to change brodacast channel avatar, and immediately apply it on the receiving device - [x] Make it possible to block InBroadcastChannel - [x] Make it possible to set the avatar of an OutgoingChannel, and apply it on the receiving side - [x] DECIDE whether we still want to use the broadcast icon as the default icon or whether we want to use the letter-in-a-circle - We decided to use the letter-in-a-circle for now, because it's easier to implement, and I need to stay in the time plan - [x] chat.rs: Return an error if the user tries to modify a `InBroadcastChannel` - [x] Add automated regression tests - [x] Grep for `broadcast` and see whether there is any other work I need to do - [x] Bug: Don't show `~` in front of the sender's same in broadcast lists ---- Note that I removed the following guard: ```rust if !new_chat_contacts.contains(&ContactId::SELF) { warn!( context, "Received group avatar update for group chat {} we are not a member of.", chat.id ); } else if !new_chat_contacts.contains(&from_id) { warn!( context, "Contact {from_id} attempts to modify group chat {} avatar without being a member.", chat.id, ); } else [...] ``` i.e. with this change, non-members will be able to modify the avatar. Things were slightly easier this way, and I think that this is in line with non-members being able to modify the group name and memberlist (they need to know the Group-Chat-Id, anyway), but I can also change it back.
This commit is contained in:
@@ -5744,9 +5744,33 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A broadcast list. See dc_chat_get_type() for details.
|
* Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
|
*
|
||||||
|
* The user can send into this chat,
|
||||||
|
* and all recipients will receive messages
|
||||||
|
* in a `DC_CHAT_TYPE_IN_BROADCAST`.
|
||||||
|
*
|
||||||
|
* Called `broadcast` here rather than `channel`,
|
||||||
|
* because the word "channel" already appears a lot in the code,
|
||||||
|
* which would make it hard to grep for it.
|
||||||
*/
|
*/
|
||||||
#define DC_CHAT_TYPE_BROADCAST 160
|
#define DC_CHAT_TYPE_OUT_BROADCAST 160
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
*
|
||||||
|
* This chat is read-only,
|
||||||
|
* and we do not know who the other recipients are.
|
||||||
|
*
|
||||||
|
* This is similar to `DC_CHAT_TYPE_MAILINGLIST`,
|
||||||
|
* with the main difference being that
|
||||||
|
* broadcasts are encrypted.
|
||||||
|
*
|
||||||
|
* Called `broadcast` here rather than `channel`,
|
||||||
|
* because the word "channel" already appears a lot in the code,
|
||||||
|
* which would make it hard to grep for it.
|
||||||
|
*/
|
||||||
|
#define DC_CHAT_TYPE_IN_BROADCAST 165
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
|
|||||||
@@ -1692,8 +1692,8 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(chat::create_broadcast_list(ctx))
|
block_on(chat::create_broadcast(ctx, "Channel".to_string()))
|
||||||
.context("Failed to create broadcast list")
|
.context("Failed to create broadcast channel")
|
||||||
.log_err(ctx)
|
.log_err(ctx)
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ impl CommandApi {
|
|||||||
/// explicitly as it may happen that oneself gets removed from a still existing
|
/// explicitly as it may happen that oneself gets removed from a still existing
|
||||||
/// group
|
/// group
|
||||||
///
|
///
|
||||||
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
/// - for broadcast channels, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||||
///
|
///
|
||||||
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
||||||
/// for now, the UI should not show the list for mailing lists.
|
/// for now, the UI should not show the list for mailing lists.
|
||||||
@@ -975,18 +975,30 @@ impl CommandApi {
|
|||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new broadcast list.
|
/// Deprecated 2025-07 in favor of create_broadcast().
|
||||||
///
|
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
|
||||||
/// however, recipients get the messages in a read-only chat
|
|
||||||
/// and will see who the other members are.
|
|
||||||
///
|
|
||||||
/// For historical reasons, this function does not take a name directly,
|
|
||||||
/// instead you have to set the name using dc_set_chat_name()
|
|
||||||
/// after creating the broadcast list.
|
|
||||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
|
self.create_broadcast(account_id, "Channel".to_string())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new **broadcast channel**
|
||||||
|
/// (called "Channel" in the UI).
|
||||||
|
///
|
||||||
|
/// Broadcast channels are similar to groups on the sending device,
|
||||||
|
/// however, recipients get the messages in a read-only chat
|
||||||
|
/// and will not see who the other members are.
|
||||||
|
///
|
||||||
|
/// Called `broadcast` here rather than `channel`,
|
||||||
|
/// because the word "channel" already appears a lot in the code,
|
||||||
|
/// which would make it hard to grep for it.
|
||||||
|
///
|
||||||
|
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||||
|
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
|
||||||
|
///
|
||||||
|
/// Returns the created chat's id.
|
||||||
|
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_broadcast_list(&ctx)
|
chat::create_broadcast(&ctx, chat_name)
|
||||||
.await
|
.await
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ pub enum ChatListItemFetchResult {
|
|||||||
is_pinned: bool,
|
is_pinned: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
/// true when chat is a broadcastlist
|
/// Deprecated 2025-07, alias for is_out_broadcast
|
||||||
is_broadcast: bool,
|
is_broadcast: bool,
|
||||||
|
/// true if the chat type is OutBroadcast
|
||||||
|
is_out_broadcast: bool,
|
||||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||||
dm_chat_contact: Option<u32>,
|
dm_chat_contact: Option<u32>,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
@@ -172,7 +174,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
is_pinned: visibility == ChatVisibility::Pinned,
|
is_pinned: visibility == ChatVisibility::Pinned,
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
is_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||||
|
is_out_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||||
dm_chat_contact,
|
dm_chat_contact,
|
||||||
was_seen_recently,
|
was_seen_recently,
|
||||||
last_message_type: message_type,
|
last_message_type: message_type,
|
||||||
|
|||||||
@@ -749,7 +749,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Group#{chat_id} created successfully.");
|
println!("Group#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
"createbroadcast" => {
|
"createbroadcast" => {
|
||||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
|
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
||||||
|
|
||||||
println!("Broadcast#{chat_id} created successfully.");
|
println!("Broadcast#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,10 +288,46 @@ class Account:
|
|||||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
After creation,
|
||||||
|
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
|
||||||
|
and is in _unpromoted_ state.
|
||||||
|
This means, you can add or remove members, change the name,
|
||||||
|
the group image and so on without messages being sent to all group members.
|
||||||
|
|
||||||
|
This changes as soon as the first message is sent to the group members
|
||||||
|
and the group becomes _promoted_.
|
||||||
|
After that, all changes are synced with all group members
|
||||||
|
by sending status message.
|
||||||
|
|
||||||
|
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
||||||
|
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
||||||
|
This may be useful if you want to show some help for just created groups.
|
||||||
|
|
||||||
|
:param protect: If set to 1 the function creates group with protection initially enabled.
|
||||||
|
Only verified members are allowed in these groups
|
||||||
|
and end-to-end-encryption is always enabled.
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
|
def create_broadcast(self, name: str) -> Chat:
|
||||||
|
"""Create a new **broadcast channel**
|
||||||
|
(called "Channel" in the UI).
|
||||||
|
|
||||||
|
Broadcast channels are similar to groups on the sending device,
|
||||||
|
however, recipients get the messages in a read-only chat
|
||||||
|
and will not see who the other members are.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
|
||||||
|
After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||||
|
see `create_group()` for more information on the unpromoted state.
|
||||||
|
|
||||||
|
Returns the created chat.
|
||||||
|
"""
|
||||||
|
return Chat(self, self._rpc.create_broadcast(self.id, name))
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||||
"""Return the Chat instance with the given ID."""
|
"""Return the Chat instance with the given ID."""
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|||||||
@@ -90,10 +90,40 @@ class ChatType(IntEnum):
|
|||||||
"""Chat type."""
|
"""Chat type."""
|
||||||
|
|
||||||
UNDEFINED = 0
|
UNDEFINED = 0
|
||||||
|
|
||||||
SINGLE = 100
|
SINGLE = 100
|
||||||
|
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||||
|
|
||||||
GROUP = 120
|
GROUP = 120
|
||||||
|
|
||||||
MAILINGLIST = 140
|
MAILINGLIST = 140
|
||||||
BROADCAST = 160
|
|
||||||
|
OUT_BROADCAST = 160
|
||||||
|
"""Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
|
The user can send into this channel,
|
||||||
|
and all recipients will receive messages
|
||||||
|
in an `IN_BROADCAST`.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
IN_BROADCAST = 165
|
||||||
|
"""Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
|
This channel is read-only,
|
||||||
|
and we do not know who the other recipients are.
|
||||||
|
|
||||||
|
This is similar to a `MAILINGLIST`,
|
||||||
|
with the main difference being that
|
||||||
|
`IN_BROADCAST`s are encrypted.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ChatVisibility(str, Enum):
|
class ChatVisibility(str, Enum):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@@ -846,3 +846,36 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
|
|||||||
assert msg.text == "hello"
|
assert msg.text == "hello"
|
||||||
|
|
||||||
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||||
|
|
||||||
|
|
||||||
|
def test_broadcast(acfactory):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
alice_chat = alice.create_broadcast("My great channel")
|
||||||
|
snapshot = alice_chat.get_basic_snapshot()
|
||||||
|
assert snapshot.name == "My great channel"
|
||||||
|
assert snapshot.is_unpromoted
|
||||||
|
assert snapshot.is_encrypted
|
||||||
|
assert snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat.add_contact(alice_contact_bob)
|
||||||
|
|
||||||
|
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
|
||||||
|
assert alice_msg.text == "hello"
|
||||||
|
assert alice_msg.show_padlock
|
||||||
|
|
||||||
|
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert bob_msg.text == "hello"
|
||||||
|
assert bob_msg.show_padlock
|
||||||
|
assert bob_msg.error is None
|
||||||
|
|
||||||
|
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
|
||||||
|
bob_chat_snapshot = bob_chat.get_basic_snapshot()
|
||||||
|
assert bob_chat_snapshot.name == "My great channel"
|
||||||
|
assert not bob_chat_snapshot.is_unpromoted
|
||||||
|
assert bob_chat_snapshot.is_encrypted
|
||||||
|
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||||
|
assert bob_chat_snapshot.is_contact_request
|
||||||
|
|
||||||
|
assert not bob_chat.can_send()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use super::*;
|
|||||||
use crate::message::{Message, Viewtype};
|
use crate::message::{Message, Viewtype};
|
||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::test_utils::{self, TestContext};
|
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
|
||||||
use crate::tools::SystemTime;
|
use crate::tools::SystemTime;
|
||||||
|
|
||||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||||
@@ -241,9 +241,8 @@ async fn test_selfavatar_in_blobdir() {
|
|||||||
async fn test_selfavatar_copy_without_recode() {
|
async fn test_selfavatar_copy_without_recode() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let avatar_src = t.dir.path().join("avatar.png");
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
|
||||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
|
||||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
|
||||||
assert!(!avatar_blob.exists());
|
assert!(!avatar_blob.exists());
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
.await
|
.await
|
||||||
@@ -251,7 +250,7 @@ async fn test_selfavatar_copy_without_recode() {
|
|||||||
assert!(avatar_blob.exists());
|
assert!(avatar_blob.exists());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||||
avatar_bytes.len() as u64
|
AVATAR_64x64_BYTES.len() as u64
|
||||||
);
|
);
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
|
|||||||
129
src/chat.rs
129
src/chat.rs
@@ -123,6 +123,9 @@ pub(crate) enum CantSendReason {
|
|||||||
/// Mailing list without known List-Post header.
|
/// Mailing list without known List-Post header.
|
||||||
ReadOnlyMailingList,
|
ReadOnlyMailingList,
|
||||||
|
|
||||||
|
/// Incoming broadcast channel where the user can't send messages.
|
||||||
|
InBroadcast,
|
||||||
|
|
||||||
/// Not a member of the chat.
|
/// Not a member of the chat.
|
||||||
NotAMember,
|
NotAMember,
|
||||||
|
|
||||||
@@ -146,6 +149,9 @@ impl fmt::Display for CantSendReason {
|
|||||||
Self::ReadOnlyMailingList => {
|
Self::ReadOnlyMailingList => {
|
||||||
write!(f, "mailing list does not have a know post address")
|
write!(f, "mailing list does not have a know post address")
|
||||||
}
|
}
|
||||||
|
Self::InBroadcast => {
|
||||||
|
write!(f, "Broadcast channel is read-only")
|
||||||
|
}
|
||||||
Self::NotAMember => write!(f, "not a member of the chat"),
|
Self::NotAMember => write!(f, "not a member of the chat"),
|
||||||
Self::MissingKey => write!(f, "key is missing"),
|
Self::MissingKey => write!(f, "key is missing"),
|
||||||
}
|
}
|
||||||
@@ -395,7 +401,7 @@ impl ChatId {
|
|||||||
let mut delete = false;
|
let mut delete = false;
|
||||||
|
|
||||||
match chat.typ {
|
match chat.typ {
|
||||||
Chattype::Broadcast => {
|
Chattype::OutBroadcast => {
|
||||||
bail!("Can't block chat of type {:?}", chat.typ)
|
bail!("Can't block chat of type {:?}", chat.typ)
|
||||||
}
|
}
|
||||||
Chattype::Single => {
|
Chattype::Single => {
|
||||||
@@ -413,7 +419,7 @@ impl ChatId {
|
|||||||
info!(context, "Can't block groups yet, deleting the chat.");
|
info!(context, "Can't block groups yet, deleting the chat.");
|
||||||
delete = true;
|
delete = true;
|
||||||
}
|
}
|
||||||
Chattype::Mailinglist => {
|
Chattype::Mailinglist | Chattype::InBroadcast => {
|
||||||
if self.set_blocked(context, Blocked::Yes).await? {
|
if self.set_blocked(context, Blocked::Yes).await? {
|
||||||
context.emit_event(EventType::ChatModified(self));
|
context.emit_event(EventType::ChatModified(self));
|
||||||
}
|
}
|
||||||
@@ -479,7 +485,7 @@ impl ChatId {
|
|||||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||||
// User has "created a chat" with all these contacts.
|
// User has "created a chat" with all these contacts.
|
||||||
//
|
//
|
||||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||||
@@ -529,7 +535,10 @@ impl ChatId {
|
|||||||
|
|
||||||
match protect {
|
match protect {
|
||||||
ProtectionStatus::Protected => match chat.typ {
|
ProtectionStatus::Protected => match chat.typ {
|
||||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {}
|
Chattype::Single
|
||||||
|
| Chattype::Group
|
||||||
|
| Chattype::OutBroadcast
|
||||||
|
| Chattype::InBroadcast => {}
|
||||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||||
},
|
},
|
||||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||||
@@ -1659,6 +1668,12 @@ impl Chat {
|
|||||||
return Ok(Some(reason));
|
return Ok(Some(reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.typ == Chattype::InBroadcast {
|
||||||
|
let reason = InBroadcast;
|
||||||
|
if !skip_fn(&reason) {
|
||||||
|
return Ok(Some(reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Do potentially slow checks last and after calls to `skip_fn` which should be fast.
|
// Do potentially slow checks last and after calls to `skip_fn` which should be fast.
|
||||||
let reason = NotAMember;
|
let reason = NotAMember;
|
||||||
@@ -1692,8 +1707,9 @@ impl Chat {
|
|||||||
/// The function does not check if the chat type allows editing of concrete elements.
|
/// The function does not check if the chat type allows editing of concrete elements.
|
||||||
pub(crate) async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
|
pub(crate) async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
|
||||||
match self.typ {
|
match self.typ {
|
||||||
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
|
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
|
||||||
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
||||||
|
Chattype::InBroadcast => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1758,8 +1774,6 @@ impl Chat {
|
|||||||
if !image_rel.is_empty() {
|
if !image_rel.is_empty() {
|
||||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||||
}
|
}
|
||||||
} else if self.typ == Chattype::Broadcast {
|
|
||||||
return Ok(Some(get_broadcast_icon(context).await?));
|
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -1872,7 +1886,7 @@ impl Chat {
|
|||||||
!self.grpid.is_empty()
|
!self.grpid.is_empty()
|
||||||
}
|
}
|
||||||
Chattype::Mailinglist => false,
|
Chattype::Mailinglist => false,
|
||||||
Chattype::Broadcast => true,
|
Chattype::OutBroadcast | Chattype::InBroadcast => true,
|
||||||
};
|
};
|
||||||
Ok(is_encrypted)
|
Ok(is_encrypted)
|
||||||
}
|
}
|
||||||
@@ -1970,7 +1984,7 @@ impl Chat {
|
|||||||
);
|
);
|
||||||
bail!("Cannot set message, contact for {} not found.", self.id);
|
bail!("Cannot set message, contact for {} not found.", self.id);
|
||||||
}
|
}
|
||||||
} else if self.typ == Chattype::Group
|
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
|
||||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||||
{
|
{
|
||||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||||
@@ -2291,7 +2305,10 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
|
Chattype::OutBroadcast
|
||||||
|
| Chattype::InBroadcast
|
||||||
|
| Chattype::Group
|
||||||
|
| Chattype::Mailinglist => {
|
||||||
if !self.grpid.is_empty() {
|
if !self.grpid.is_empty() {
|
||||||
return Ok(Some(SyncId::Grpid(self.grpid.clone())));
|
return Ok(Some(SyncId::Grpid(self.grpid.clone())));
|
||||||
}
|
}
|
||||||
@@ -2465,15 +2482,6 @@ pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<PathBuf> {
|
|
||||||
get_asset_icon(
|
|
||||||
context,
|
|
||||||
"icon-broadcast",
|
|
||||||
include_bytes!("../assets/icon-broadcast.png"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
||||||
get_asset_icon(
|
get_asset_icon(
|
||||||
context,
|
context,
|
||||||
@@ -3614,37 +3622,27 @@ pub async fn create_group_chat(
|
|||||||
Ok(chat_id)
|
Ok(chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds an unused name for a new broadcast list.
|
/// Create a new **broadcast channel**
|
||||||
async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
/// (called "Channel" in the UI).
|
||||||
let base_name = stock_str::broadcast_list(context).await;
|
///
|
||||||
for attempt in 1..1000 {
|
/// Broadcast channels are similar to groups on the sending device,
|
||||||
let better_name = if attempt > 1 {
|
/// however, recipients get the messages in a read-only chat
|
||||||
format!("{base_name} {attempt}")
|
/// and will not see who the other members are.
|
||||||
} else {
|
///
|
||||||
base_name.clone()
|
/// Called `broadcast` here rather than `channel`,
|
||||||
};
|
/// because the word "channel" already appears a lot in the code,
|
||||||
if !context
|
/// which would make it hard to grep for it.
|
||||||
.sql
|
///
|
||||||
.exists(
|
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||||
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
|
/// see [`create_group_chat`] for more information on the unpromoted state.
|
||||||
(Chattype::Broadcast, &better_name),
|
///
|
||||||
)
|
/// Returns the created chat's id.
|
||||||
.await?
|
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||||
{
|
|
||||||
return Ok(better_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(base_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new broadcast list.
|
|
||||||
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
|
||||||
let chat_name = find_unused_broadcast_list_name(context).await?;
|
|
||||||
let grpid = create_id();
|
let grpid = create_id();
|
||||||
create_broadcast_list_ex(context, Sync, grpid, chat_name).await
|
create_broadcast_ex(context, Sync, grpid, chat_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_broadcast_list_ex(
|
pub(crate) async fn create_broadcast_ex(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
sync: sync::Sync,
|
sync: sync::Sync,
|
||||||
grpid: String,
|
grpid: String,
|
||||||
@@ -3659,7 +3657,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
|||||||
if cnt == 1 {
|
if cnt == 1 {
|
||||||
return Ok(t.query_row(
|
return Ok(t.query_row(
|
||||||
"SELECT id FROM chats WHERE grpid=? AND type=?",
|
"SELECT id FROM chats WHERE grpid=? AND type=?",
|
||||||
(grpid, Chattype::Broadcast),
|
(grpid, Chattype::OutBroadcast),
|
||||||
|row| {
|
|row| {
|
||||||
let id: isize = row.get(0)?;
|
let id: isize = row.get(0)?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
@@ -3671,7 +3669,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
|||||||
(type, name, grpid, param, created_timestamp) \
|
(type, name, grpid, param, created_timestamp) \
|
||||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||||
(
|
(
|
||||||
Chattype::Broadcast,
|
Chattype::OutBroadcast,
|
||||||
&chat_name,
|
&chat_name,
|
||||||
&grpid,
|
&grpid,
|
||||||
create_smeared_timestamp(context),
|
create_smeared_timestamp(context),
|
||||||
@@ -3809,7 +3807,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
// this also makes sure, no contacts are added to special or normal chats
|
// this also makes sure, no contacts are added to special or normal chats
|
||||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||||
"{} is not a group/broadcast where one can add members",
|
"{} is not a group/broadcast where one can add members",
|
||||||
chat_id
|
chat_id
|
||||||
);
|
);
|
||||||
@@ -3820,8 +3818,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
);
|
);
|
||||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
|
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
|
||||||
"Cannot add SELF to broadcast."
|
"Cannot add SELF to broadcast channel."
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.is_encrypted(context).await? == contact.is_key_contact(),
|
chat.is_encrypted(context).await? == contact.is_key_contact(),
|
||||||
@@ -4040,7 +4038,7 @@ pub async fn remove_contact_from_chat(
|
|||||||
let mut msg = Message::new(Viewtype::default());
|
let mut msg = Message::new(Viewtype::default());
|
||||||
|
|
||||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||||
if !chat.is_self_in_chat(context).await? {
|
if !chat.is_self_in_chat(context).await? {
|
||||||
let err_msg = format!(
|
let err_msg = format!(
|
||||||
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
||||||
@@ -4148,7 +4146,7 @@ async fn rename_ex(
|
|||||||
|
|
||||||
if chat.typ == Chattype::Group
|
if chat.typ == Chattype::Group
|
||||||
|| chat.typ == Chattype::Mailinglist
|
|| chat.typ == Chattype::Mailinglist
|
||||||
|| chat.typ == Chattype::Broadcast
|
|| chat.typ == Chattype::OutBroadcast
|
||||||
{
|
{
|
||||||
if chat.name == new_name {
|
if chat.name == new_name {
|
||||||
success = true;
|
success = true;
|
||||||
@@ -4166,7 +4164,6 @@ async fn rename_ex(
|
|||||||
.await?;
|
.await?;
|
||||||
if chat.is_promoted()
|
if chat.is_promoted()
|
||||||
&& !chat.is_mailing_list()
|
&& !chat.is_mailing_list()
|
||||||
&& chat.typ != Chattype::Broadcast
|
|
||||||
&& sanitize_single_line(&chat.name) != new_name
|
&& sanitize_single_line(&chat.name) != new_name
|
||||||
{
|
{
|
||||||
msg.viewtype = Viewtype::Text;
|
msg.viewtype = Viewtype::Text;
|
||||||
@@ -4212,15 +4209,15 @@ pub async fn set_chat_profile_image(
|
|||||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ == Chattype::Group,
|
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||||
"Can only set profile image for group chats"
|
"Can only set profile image for groups / broadcasts"
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
!chat.grpid.is_empty(),
|
!chat.grpid.is_empty(),
|
||||||
"Cannot set profile image for ad hoc groups"
|
"Cannot set profile image for ad hoc groups"
|
||||||
);
|
);
|
||||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||||
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
if !chat.is_self_in_chat(context).await? {
|
||||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||||
"Cannot set chat profile image; self not in group.".into(),
|
"Cannot set chat profile image; self not in group.".into(),
|
||||||
));
|
));
|
||||||
@@ -4287,10 +4284,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
|||||||
bail!("cannot forward drafts.");
|
bail!("cannot forward drafts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// we tested a sort of broadcast
|
|
||||||
// by not marking own forwarded messages as such,
|
|
||||||
// however, this turned out to be to confusing and unclear.
|
|
||||||
|
|
||||||
if msg.get_viewtype() != Viewtype::Sticker {
|
if msg.get_viewtype() != Viewtype::Sticker {
|
||||||
msg.param
|
msg.param
|
||||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||||
@@ -4787,7 +4780,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
|||||||
"Cannot add address-contacts to encrypted chat {id}"
|
"Cannot add address-contacts to encrypted chat {id}"
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ == Chattype::Broadcast,
|
chat.typ == Chattype::OutBroadcast,
|
||||||
"{id} is not a broadcast list",
|
"{id} is not a broadcast list",
|
||||||
);
|
);
|
||||||
let mut contacts = HashSet::new();
|
let mut contacts = HashSet::new();
|
||||||
@@ -4808,7 +4801,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
|||||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||||
|
|
||||||
// We do not care about `add_timestamp` column
|
// We do not care about `add_timestamp` column
|
||||||
// because timestamps are not used for broadcast lists.
|
// because timestamps are not used for broadcast channels.
|
||||||
let mut statement = transaction
|
let mut statement = transaction
|
||||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||||
for contact_id in &contacts {
|
for contact_id in &contacts {
|
||||||
@@ -4835,7 +4828,7 @@ async fn set_contacts_by_fingerprints(
|
|||||||
"Cannot add key-contacts to unencrypted chat {id}"
|
"Cannot add key-contacts to unencrypted chat {id}"
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ == Chattype::Broadcast,
|
chat.typ == Chattype::OutBroadcast,
|
||||||
"{id} is not a broadcast list",
|
"{id} is not a broadcast list",
|
||||||
);
|
);
|
||||||
let mut contacts = HashSet::new();
|
let mut contacts = HashSet::new();
|
||||||
@@ -4857,7 +4850,7 @@ async fn set_contacts_by_fingerprints(
|
|||||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||||
|
|
||||||
// We do not care about `add_timestamp` column
|
// We do not care about `add_timestamp` column
|
||||||
// because timestamps are not used for broadcast lists.
|
// because timestamps are not used for broadcast channels.
|
||||||
let mut statement = transaction
|
let mut statement = transaction
|
||||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||||
for contact_id in &contacts {
|
for contact_id in &contacts {
|
||||||
@@ -4895,7 +4888,7 @@ pub(crate) enum SyncAction {
|
|||||||
Accept,
|
Accept,
|
||||||
SetVisibility(ChatVisibility),
|
SetVisibility(ChatVisibility),
|
||||||
SetMuted(MuteDuration),
|
SetMuted(MuteDuration),
|
||||||
/// Create broadcast list with the given name.
|
/// Create broadcast channel with the given name.
|
||||||
CreateBroadcast(String),
|
CreateBroadcast(String),
|
||||||
Rename(String),
|
Rename(String),
|
||||||
/// Set chat contacts by their addresses.
|
/// Set chat contacts by their addresses.
|
||||||
@@ -4960,7 +4953,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
SyncId::Grpid(grpid) => {
|
SyncId::Grpid(grpid) => {
|
||||||
if let SyncAction::CreateBroadcast(name) = action {
|
if let SyncAction::CreateBroadcast(name) = action {
|
||||||
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
get_chat_id_by_grpid(self, grpid)
|
get_chat_id_by_grpid(self, grpid)
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use crate::headerdef::HeaderDef;
|
|||||||
use crate::imex::{ImexMode, has_backup, imex};
|
use crate::imex::{ImexMode, has_backup, imex};
|
||||||
use crate::message::{MessengerMessage, delete_msgs};
|
use crate::message::{MessengerMessage, delete_msgs};
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
use crate::test_utils::{TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
|
use crate::test_utils::{
|
||||||
|
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||||
|
TimeShiftFalsePositiveNote, sync,
|
||||||
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@@ -2259,7 +2262,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
|||||||
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
|
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
|
||||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||||
add_contact_to_chat(&bob, group_id, charlie_id).await?;
|
add_contact_to_chat(&bob, group_id, charlie_id).await?;
|
||||||
let broadcast_id = create_broadcast_list(&bob).await?;
|
let broadcast_id = create_broadcast(&bob, "Channel".to_string()).await?;
|
||||||
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
|
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
|
||||||
for chat_id in &[single_id, group_id, broadcast_id] {
|
for chat_id in &[single_id, group_id, broadcast_id] {
|
||||||
forward_msgs(&bob, &[orig_msg.id], *chat_id).await?;
|
forward_msgs(&bob, &[orig_msg.id], *chat_id).await?;
|
||||||
@@ -2619,8 +2622,8 @@ async fn test_broadcast() -> Result<()> {
|
|||||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||||
assert!(msg.get_showpadlock());
|
assert!(msg.get_showpadlock());
|
||||||
|
|
||||||
// test broadcast list
|
// test broadcast channel
|
||||||
let broadcast_id = create_broadcast_list(&alice).await?;
|
let broadcast_id = create_broadcast(&alice, "Channel".to_string()).await?;
|
||||||
add_contact_to_chat(
|
add_contact_to_chat(
|
||||||
&alice,
|
&alice,
|
||||||
broadcast_id,
|
broadcast_id,
|
||||||
@@ -2629,11 +2632,11 @@ async fn test_broadcast() -> Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||||
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
|
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
|
||||||
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
|
set_chat_name(&alice, broadcast_id, "Broadcast channel").await?;
|
||||||
{
|
{
|
||||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
assert_eq!(chat.typ, Chattype::OutBroadcast);
|
||||||
assert_eq!(chat.name, "Broadcast list");
|
assert_eq!(chat.name, "Broadcast channel");
|
||||||
assert!(!chat.is_self_talk());
|
assert!(!chat.is_self_talk());
|
||||||
|
|
||||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||||
@@ -2649,12 +2652,13 @@ async fn test_broadcast() -> Result<()> {
|
|||||||
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
|
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
|
||||||
let msg = bob.recv_msg(&sent_msg).await;
|
let msg = bob.recv_msg(&sent_msg).await;
|
||||||
assert_eq!(msg.get_text(), "ola!");
|
assert_eq!(msg.get_text(), "ola!");
|
||||||
assert_eq!(msg.subject, "Broadcast list");
|
assert_eq!(msg.subject, "Broadcast channel");
|
||||||
assert!(msg.get_showpadlock());
|
assert!(msg.get_showpadlock());
|
||||||
|
assert!(msg.get_override_sender_name().is_none());
|
||||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
assert_eq!(chat.typ, Chattype::InBroadcast);
|
||||||
assert_ne!(chat.id, chat_bob.id);
|
assert_ne!(chat.id, chat_bob.id);
|
||||||
assert_eq!(chat.name, "Broadcast list");
|
assert_eq!(chat.name, "Broadcast channel");
|
||||||
assert!(!chat.is_self_talk());
|
assert!(!chat.is_self_talk());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2672,6 +2676,14 @@ async fn test_broadcast() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// - Alice has multiple devices
|
||||||
|
/// - Alice creates a broadcast and sends a message into it
|
||||||
|
/// - Alice's second device sees the broadcast
|
||||||
|
/// - Alice adds Bob to the broadcast
|
||||||
|
/// - Synchronization is only implemented via sync messages for now,
|
||||||
|
/// which are not enabled in tests by default,
|
||||||
|
/// so, Alice's second device doesn't see the change yet.
|
||||||
|
/// `test_sync_broadcast()` tests that synchronization works via sync messages.
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_broadcast_multidev() -> Result<()> {
|
async fn test_broadcast_multidev() -> Result<()> {
|
||||||
let alices = [
|
let alices = [
|
||||||
@@ -2681,9 +2693,9 @@ async fn test_broadcast_multidev() -> Result<()> {
|
|||||||
let bob = TestContext::new_bob().await;
|
let bob = TestContext::new_bob().await;
|
||||||
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
|
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
|
||||||
|
|
||||||
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
|
let a0_broadcast_id = create_broadcast(&alices[0], "Channel".to_string()).await?;
|
||||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||||
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast channel 42").await?;
|
||||||
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
|
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
|
||||||
let msg = alices[1].recv_msg(&sent_msg).await;
|
let msg = alices[1].recv_msg(&sent_msg).await;
|
||||||
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
||||||
@@ -2692,8 +2704,8 @@ async fn test_broadcast_multidev() -> Result<()> {
|
|||||||
.0;
|
.0;
|
||||||
assert_eq!(msg.chat_id, a1_broadcast_id);
|
assert_eq!(msg.chat_id, a1_broadcast_id);
|
||||||
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
||||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||||
assert!(
|
assert!(
|
||||||
get_chat_contacts(&alices[1], a1_broadcast_id)
|
get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||||
.await?
|
.await?
|
||||||
@@ -2701,13 +2713,13 @@ async fn test_broadcast_multidev() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
|
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
|
||||||
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?;
|
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast channel 43").await?;
|
||||||
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
||||||
let msg = alices[0].recv_msg(&sent_msg).await;
|
let msg = alices[0].recv_msg(&sent_msg).await;
|
||||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||||
assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast);
|
assert_eq!(a0_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||||
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42");
|
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||||
assert!(
|
assert!(
|
||||||
get_chat_contacts(&alices[0], a0_broadcast_id)
|
get_chat_contacts(&alices[0], a0_broadcast_id)
|
||||||
.await?
|
.await?
|
||||||
@@ -2717,6 +2729,161 @@ async fn test_broadcast_multidev() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// - Create a broadcast channel
|
||||||
|
/// - Send a message into it in order to promote it
|
||||||
|
/// - Add a contact
|
||||||
|
/// - Rename it
|
||||||
|
/// - the change should be visible on the receiver's side immediately
|
||||||
|
/// - Change the avatar
|
||||||
|
/// - The change should be visible on the receiver's side immediately
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_broadcasts_name_and_avatar() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
alice.set_config(Config::Displayname, Some("Alice")).await?;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||||
|
|
||||||
|
tcm.section("Create a broadcast channel");
|
||||||
|
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||||
|
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||||
|
assert_eq!(alice_chat.typ, Chattype::OutBroadcast);
|
||||||
|
|
||||||
|
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||||
|
assert_eq!(alice_chat.is_promoted(), false);
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Hi nobody").await;
|
||||||
|
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||||
|
assert_eq!(alice_chat.is_promoted(), true);
|
||||||
|
assert_eq!(sent.recipients, "alice@example.org");
|
||||||
|
|
||||||
|
tcm.section("Add a contact to the chat and send a message");
|
||||||
|
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||||
|
|
||||||
|
assert_eq!(sent.recipients, "bob@example.net alice@example.org");
|
||||||
|
let rcvd = bob.recv_msg(&sent).await;
|
||||||
|
assert!(rcvd.get_override_sender_name().is_none());
|
||||||
|
assert_eq!(rcvd.text, "Hi somebody");
|
||||||
|
let bob_chat = Chat::load_from_db(bob, rcvd.chat_id).await?;
|
||||||
|
assert_eq!(bob_chat.typ, Chattype::InBroadcast);
|
||||||
|
assert_eq!(bob_chat.name, "My Channel");
|
||||||
|
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||||
|
|
||||||
|
tcm.section("Change broadcast channel name, and check that receivers see it");
|
||||||
|
set_chat_name(alice, alice_chat_id, "New Channel name").await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
let rcvd = bob.recv_msg(&sent).await;
|
||||||
|
assert!(rcvd.get_override_sender_name().is_none());
|
||||||
|
assert_eq!(rcvd.get_info_type(), SystemMessage::GroupNameChanged);
|
||||||
|
assert_eq!(
|
||||||
|
rcvd.text,
|
||||||
|
r#"Group name changed from "My Channel" to "New Channel name" by Alice."#
|
||||||
|
);
|
||||||
|
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||||
|
assert_eq!(bob_chat.name, "New Channel name");
|
||||||
|
|
||||||
|
tcm.section("Set a broadcast channel avatar, and check that receivers see it");
|
||||||
|
let file = alice.get_blobdir().join("avatar.png");
|
||||||
|
tokio::fs::write(&file, AVATAR_64x64_BYTES).await?;
|
||||||
|
set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||||
|
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||||
|
|
||||||
|
let rcvd = bob.recv_msg(&sent).await;
|
||||||
|
assert!(rcvd.get_override_sender_name().is_none());
|
||||||
|
assert_eq!(rcvd.get_info_type(), SystemMessage::GroupImageChanged);
|
||||||
|
assert_eq!(rcvd.text, "Group image changed by Alice.");
|
||||||
|
assert_eq!(rcvd.chat_id, bob_chat.id);
|
||||||
|
|
||||||
|
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||||
|
let avatar = bob_chat.get_profile_image(bob).await?.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
avatar.file_name().unwrap().to_str().unwrap(),
|
||||||
|
AVATAR_64x64_DEDUPLICATED
|
||||||
|
);
|
||||||
|
|
||||||
|
tcm.section("Check that Bob can't modify the broadcast channel");
|
||||||
|
set_chat_profile_image(bob, bob_chat.id, file.to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
set_chat_name(bob, bob_chat.id, "Bob Channel name")
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Create a broadcast channel
|
||||||
|
/// - Block it
|
||||||
|
/// - Check that the broadcast channel appears in the list of blocked contacts
|
||||||
|
/// - A message is sent into the broadcast channel, but it is blocked
|
||||||
|
/// - Unblock it
|
||||||
|
/// - Receive a message again in the now-unblocked broadcast channel
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_block_broadcast() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||||
|
|
||||||
|
tcm.section("Create a broadcast channel with Bob, and send a message");
|
||||||
|
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||||
|
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||||
|
let rcvd = bob.recv_msg(&sent).await;
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||||
|
assert_eq!(chats.len(), 1);
|
||||||
|
assert_eq!(chats.get_chat_id(0)?, rcvd.chat_id);
|
||||||
|
|
||||||
|
assert_eq!(rcvd.chat_blocked, Blocked::Request);
|
||||||
|
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||||
|
assert_eq!(blocked.len(), 0);
|
||||||
|
|
||||||
|
tcm.section("Bob blocks the chat");
|
||||||
|
rcvd.chat_id.block(bob).await?;
|
||||||
|
let chat = Chat::load_from_db(bob, rcvd.chat_id).await?;
|
||||||
|
assert_eq!(chat.blocked, Blocked::Yes);
|
||||||
|
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||||
|
assert_eq!(blocked.len(), 1);
|
||||||
|
let blocked = Contact::get_by_id(bob, blocked[0]).await?;
|
||||||
|
assert!(blocked.is_key_contact());
|
||||||
|
assert_eq!(blocked.origin, Origin::MailinglistAddress);
|
||||||
|
assert_eq!(blocked.get_name(), "My Channel");
|
||||||
|
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Second message").await;
|
||||||
|
let rcvd2 = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(rcvd2.chat_id, rcvd.chat_id);
|
||||||
|
assert_eq!(rcvd2.chat_blocked, Blocked::Yes);
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
|
tcm.section("Bob unblocks the chat");
|
||||||
|
Contact::unblock(bob, blocked.id).await?;
|
||||||
|
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Third message").await;
|
||||||
|
let rcvd3 = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(rcvd3.chat_id, rcvd.chat_id);
|
||||||
|
assert_eq!(rcvd3.chat_blocked, Blocked::Not);
|
||||||
|
|
||||||
|
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||||
|
assert_eq!(blocked.len(), 0);
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||||
|
assert_eq!(chats.len(), 1);
|
||||||
|
assert_eq!(chats.get_chat_id(0)?, rcvd.chat_id);
|
||||||
|
|
||||||
|
let chat = Chat::load_from_db(bob, rcvd3.chat_id).await?;
|
||||||
|
assert_eq!(chat.blocked, Blocked::Not);
|
||||||
|
assert_eq!(chat.name, "My Channel");
|
||||||
|
assert_eq!(chat.typ, Chattype::InBroadcast);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
@@ -3403,6 +3570,7 @@ async fn test_sync_muted() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests that synchronizing broadcast channels via sync-messages works
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_sync_broadcast() -> Result<()> {
|
async fn test_sync_broadcast() -> Result<()> {
|
||||||
let mut tcm = TestContextManager::new();
|
let mut tcm = TestContextManager::new();
|
||||||
@@ -3414,7 +3582,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
|||||||
let bob = &tcm.bob().await;
|
let bob = &tcm.bob().await;
|
||||||
let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id;
|
let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id;
|
||||||
|
|
||||||
let a0_broadcast_id = create_broadcast_list(alice0).await?;
|
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
|
||||||
sync(alice0, alice1).await;
|
sync(alice0, alice1).await;
|
||||||
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
||||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||||
@@ -3422,7 +3590,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.0;
|
.0;
|
||||||
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
||||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||||
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
|
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
|
||||||
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
||||||
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||||
@@ -3440,7 +3608,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
|||||||
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
|
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
|
||||||
let msg = bob.recv_msg(&sent_msg).await;
|
let msg = bob.recv_msg(&sent_msg).await;
|
||||||
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||||
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
assert_eq!(chat.get_type(), Chattype::InBroadcast);
|
||||||
let msg = alice0.recv_msg(&sent_msg).await;
|
let msg = alice0.recv_msg(&sent_msg).await;
|
||||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||||
remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||||
@@ -3465,18 +3633,18 @@ async fn test_sync_name() -> Result<()> {
|
|||||||
for a in [alice0, alice1] {
|
for a in [alice0, alice1] {
|
||||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||||
}
|
}
|
||||||
let a0_broadcast_id = create_broadcast_list(alice0).await?;
|
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
|
||||||
sync(alice0, alice1).await;
|
sync(alice0, alice1).await;
|
||||||
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
||||||
set_chat_name(alice0, a0_broadcast_id, "Broadcast list 42").await?;
|
set_chat_name(alice0, a0_broadcast_id, "Broadcast channel 42").await?;
|
||||||
sync(alice0, alice1).await;
|
sync(alice0, alice1).await;
|
||||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||||
.await?
|
.await?
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0;
|
.0;
|
||||||
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
||||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,8 @@ impl Chatlist {
|
|||||||
if lastmsg.from_id == ContactId::SELF {
|
if lastmsg.from_id == ContactId::SELF {
|
||||||
None
|
None
|
||||||
} else if chat.typ == Chattype::Group
|
} else if chat.typ == Chattype::Group
|
||||||
|| chat.typ == Chattype::Broadcast
|
|| chat.typ == Chattype::OutBroadcast
|
||||||
|
|| chat.typ == Chattype::InBroadcast
|
||||||
|| chat.typ == Chattype::Mailinglist
|
|| chat.typ == Chattype::Mailinglist
|
||||||
|| chat.is_self_talk()
|
|| chat.is_self_talk()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -126,17 +126,46 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
|||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Chattype {
|
pub enum Chattype {
|
||||||
/// 1:1 chat.
|
/// A 1:1 chat, i.e. a normal chat with a single contact.
|
||||||
|
///
|
||||||
|
/// Created by [`ChatId::create_for_contact`].
|
||||||
Single = 100,
|
Single = 100,
|
||||||
|
|
||||||
/// Group chat.
|
/// Group chat.
|
||||||
|
///
|
||||||
|
/// Created by [`crate::chat::create_group_chat`].
|
||||||
Group = 120,
|
Group = 120,
|
||||||
|
|
||||||
/// Mailing list.
|
/// An (unencrypted) mailing list,
|
||||||
|
/// created by an incoming mailing list email.
|
||||||
Mailinglist = 140,
|
Mailinglist = 140,
|
||||||
|
|
||||||
/// Broadcast list.
|
/// Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
Broadcast = 160,
|
///
|
||||||
|
/// The user can send into this chat,
|
||||||
|
/// and all recipients will receive messages
|
||||||
|
/// in an `InBroadcast`.
|
||||||
|
///
|
||||||
|
/// Called `broadcast` here rather than `channel`,
|
||||||
|
/// because the word "channel" already appears a lot in the code,
|
||||||
|
/// which would make it hard to grep for it.
|
||||||
|
///
|
||||||
|
/// Created by [`crate::chat::create_broadcast`].
|
||||||
|
OutBroadcast = 160,
|
||||||
|
|
||||||
|
/// Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
///
|
||||||
|
/// This chat is read-only,
|
||||||
|
/// and we do not know who the other recipients are.
|
||||||
|
///
|
||||||
|
/// This is similar to a `MailingList`,
|
||||||
|
/// with the main difference being that
|
||||||
|
/// `InBroadcast`s are encrypted.
|
||||||
|
///
|
||||||
|
/// Called `broadcast` here rather than `channel`,
|
||||||
|
/// because the word "channel" already appears a lot in the code,
|
||||||
|
/// which would make it hard to grep for it.
|
||||||
|
InBroadcast = 165,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||||
@@ -239,7 +268,7 @@ mod tests {
|
|||||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
||||||
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
||||||
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
|
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1179,7 +1179,7 @@ impl Contact {
|
|||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds blocked mailinglists as contacts
|
/// Adds blocked mailinglists and broadcast channels as pseudo-contacts
|
||||||
/// to allow unblocking them as if they are contacts
|
/// to allow unblocking them as if they are contacts
|
||||||
/// (this way, only one unblock-ffi is needed and only one set of ui-functions,
|
/// (this way, only one unblock-ffi is needed and only one set of ui-functions,
|
||||||
/// from the users perspective,
|
/// from the users perspective,
|
||||||
@@ -1188,15 +1188,20 @@ impl Contact {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.transaction(move |transaction| {
|
.transaction(move |transaction| {
|
||||||
let mut stmt = transaction
|
let mut stmt = transaction.prepare(
|
||||||
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
|
"SELECT name, grpid, type FROM chats WHERE (type=? OR type=?) AND blocked=?",
|
||||||
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
|
)?;
|
||||||
|
let rows = stmt.query_map(
|
||||||
|
(Chattype::Mailinglist, Chattype::InBroadcast, Blocked::Yes),
|
||||||
|
|row| {
|
||||||
let name: String = row.get(0)?;
|
let name: String = row.get(0)?;
|
||||||
let grpid: String = row.get(1)?;
|
let grpid: String = row.get(1)?;
|
||||||
Ok((name, grpid))
|
let typ: Chattype = row.get(2)?;
|
||||||
})?;
|
Ok((name, grpid, typ))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
let blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
let blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
for (name, grpid) in blocked_mailinglists {
|
for (name, grpid, typ) in blocked_mailinglists {
|
||||||
let count = transaction.query_row(
|
let count = transaction.query_row(
|
||||||
"SELECT COUNT(id) FROM contacts WHERE addr=?",
|
"SELECT COUNT(id) FROM contacts WHERE addr=?",
|
||||||
[&grpid],
|
[&grpid],
|
||||||
@@ -1209,10 +1214,17 @@ impl Contact {
|
|||||||
transaction.execute("INSERT INTO contacts (addr) VALUES (?)", [&grpid])?;
|
transaction.execute("INSERT INTO contacts (addr) VALUES (?)", [&grpid])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fingerprint = if typ == Chattype::InBroadcast {
|
||||||
|
// Set some fingerprint so that is_pgp_contact() returns true,
|
||||||
|
// and the contact isn't marked with a letter icon.
|
||||||
|
"Blocked_broadcast"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
// Always do an update in case the blocking is reset or name is changed.
|
// Always do an update in case the blocking is reset or name is changed.
|
||||||
transaction.execute(
|
transaction.execute(
|
||||||
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
|
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
|
||||||
(&name, Origin::MailinglistAddress, &grpid),
|
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ mod test_chatlist_events {
|
|||||||
use crate::{
|
use crate::{
|
||||||
EventType,
|
EventType,
|
||||||
chat::{
|
chat::{
|
||||||
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast_list,
|
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
|
||||||
create_group_chat, set_muted,
|
create_group_chat, set_muted,
|
||||||
},
|
},
|
||||||
config::Config,
|
config::Config,
|
||||||
@@ -308,13 +308,13 @@ mod test_chatlist_events {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create broadcastlist
|
/// Create broadcast channel
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_create_broadcastlist() -> Result<()> {
|
async fn test_create_broadcast() -> Result<()> {
|
||||||
let mut tcm = TestContextManager::new();
|
let mut tcm = TestContextManager::new();
|
||||||
let alice = tcm.alice().await;
|
let alice = tcm.alice().await;
|
||||||
alice.evtracker.clear_events();
|
alice.evtracker.clear_events();
|
||||||
create_broadcast_list(&alice).await?;
|
create_broadcast(&alice, "Channel".to_string()).await?;
|
||||||
wait_for_chatlist(&alice).await;
|
wait_for_chatlist(&alice).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -855,9 +855,10 @@ impl Message {
|
|||||||
|
|
||||||
let contact = if self.from_id != ContactId::SELF {
|
let contact = if self.from_id != ContactId::SELF {
|
||||||
match chat.typ {
|
match chat.typ {
|
||||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
Chattype::Group
|
||||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
| Chattype::OutBroadcast
|
||||||
}
|
| Chattype::InBroadcast
|
||||||
|
| Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
|
||||||
Chattype::Single => None,
|
Chattype::Single => None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ pub struct MimeFactory {
|
|||||||
/// because in case of "member removed" message
|
/// because in case of "member removed" message
|
||||||
/// removed member is in the recipient list,
|
/// removed member is in the recipient list,
|
||||||
/// but not in the `To` header.
|
/// but not in the `To` header.
|
||||||
/// In case of broadcast lists there are multiple recipients,
|
/// In case of broadcast channels there are multiple recipients,
|
||||||
/// but the `To` header has no members.
|
/// but the `To` header has no members.
|
||||||
///
|
///
|
||||||
/// If `bcc_self` configuration is enabled,
|
/// If `bcc_self` configuration is enabled,
|
||||||
@@ -98,7 +98,7 @@ pub struct MimeFactory {
|
|||||||
/// Vector of pairs of recipient name and address that goes into the `To` field.
|
/// Vector of pairs of recipient name and address that goes into the `To` field.
|
||||||
///
|
///
|
||||||
/// The list of actual message recipient addresses may be different,
|
/// The list of actual message recipient addresses may be different,
|
||||||
/// e.g. if members are hidden for broadcast lists
|
/// e.g. if members are hidden for broadcast channels
|
||||||
/// or if the keys for some recipients are missing
|
/// or if the keys for some recipients are missing
|
||||||
/// and encrypted message cannot be sent to them.
|
/// and encrypted message cannot be sent to them.
|
||||||
to: Vec<(String, String)>,
|
to: Vec<(String, String)>,
|
||||||
@@ -178,7 +178,7 @@ impl MimeFactory {
|
|||||||
let now = time();
|
let now = time();
|
||||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||||
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
||||||
let undisclosed_recipients = chat.typ == Chattype::Broadcast;
|
let undisclosed_recipients = chat.typ == Chattype::OutBroadcast;
|
||||||
|
|
||||||
let from_addr = context.get_primary_self_addr().await?;
|
let from_addr = context.get_primary_self_addr().await?;
|
||||||
let config_displayname = context
|
let config_displayname = context
|
||||||
@@ -599,7 +599,7 @@ impl MimeFactory {
|
|||||||
return Ok(msg.subject.clone());
|
return Ok(msg.subject.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast)
|
if (chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast)
|
||||||
&& quoted_msg_subject.is_none_or_empty()
|
&& quoted_msg_subject.is_none_or_empty()
|
||||||
{
|
{
|
||||||
let re = if self.in_reply_to.is_empty() {
|
let re = if self.in_reply_to.is_empty() {
|
||||||
@@ -791,7 +791,7 @@ impl MimeFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||||
if chat.typ == Chattype::Broadcast {
|
if chat.typ == Chattype::OutBroadcast {
|
||||||
headers.push((
|
headers.push((
|
||||||
"List-ID",
|
"List-ID",
|
||||||
mail_builder::headers::text::Text::new(format!(
|
mail_builder::headers::text::Text::new(format!(
|
||||||
@@ -1035,7 +1035,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
match &self.loaded {
|
match &self.loaded {
|
||||||
Loaded::Message { chat, msg } => {
|
Loaded::Message { chat, msg } => {
|
||||||
if chat.typ != Chattype::Broadcast {
|
if chat.typ != Chattype::OutBroadcast {
|
||||||
for (addr, key) in &encryption_keys {
|
for (addr, key) in &encryption_keys {
|
||||||
let fingerprint = key.dc_fingerprint().hex();
|
let fingerprint = key.dc_fingerprint().hex();
|
||||||
let cmd = msg.param.get_cmd();
|
let cmd = msg.param.get_cmd();
|
||||||
@@ -1300,9 +1300,9 @@ impl MimeFactory {
|
|||||||
let send_verified_headers = match chat.typ {
|
let send_verified_headers = match chat.typ {
|
||||||
Chattype::Single => true,
|
Chattype::Single => true,
|
||||||
Chattype::Group => true,
|
Chattype::Group => true,
|
||||||
// Mailinglists and broadcast lists can actually never be verified:
|
// Mailinglists and broadcast channels can actually never be verified:
|
||||||
Chattype::Mailinglist => false,
|
Chattype::Mailinglist => false,
|
||||||
Chattype::Broadcast => false,
|
Chattype::OutBroadcast | Chattype::InBroadcast => false,
|
||||||
};
|
};
|
||||||
if chat.is_protected() && send_verified_headers {
|
if chat.is_protected() && send_verified_headers {
|
||||||
headers.push((
|
headers.push((
|
||||||
@@ -1311,7 +1311,7 @@ impl MimeFactory {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if chat.typ == Chattype::Group {
|
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||||
// Send group ID unless it is an ad hoc group that has no ID.
|
// Send group ID unless it is an ad hoc group that has no ID.
|
||||||
if !chat.grpid.is_empty() {
|
if !chat.grpid.is_empty() {
|
||||||
headers.push((
|
headers.push((
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result, ensure};
|
||||||
use data_encoding::BASE32_NOPAD;
|
use data_encoding::BASE32_NOPAD;
|
||||||
use deltachat_contact_tools::{
|
use deltachat_contact_tools::{
|
||||||
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
|
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
|
||||||
@@ -93,10 +93,10 @@ enum ChatAssignment {
|
|||||||
/// assign to encrypted group.
|
/// assign to encrypted group.
|
||||||
GroupChat { grpid: String },
|
GroupChat { grpid: String },
|
||||||
|
|
||||||
/// Mailing list or broadcast list.
|
/// Mailing list or broadcast channel.
|
||||||
///
|
///
|
||||||
/// Mailing lists don't have members.
|
/// Mailing lists don't have members.
|
||||||
/// Broadcast lists have members
|
/// Broadcast channels have members
|
||||||
/// on the sender side,
|
/// on the sender side,
|
||||||
/// but their addresses don't go into
|
/// but their addresses don't go into
|
||||||
/// the `To` field.
|
/// the `To` field.
|
||||||
@@ -107,7 +107,7 @@ enum ChatAssignment {
|
|||||||
/// up except the `from_id`
|
/// up except the `from_id`
|
||||||
/// which may be an email address contact
|
/// which may be an email address contact
|
||||||
/// or a key-contact.
|
/// or a key-contact.
|
||||||
MailingList,
|
MailingListOrBroadcast,
|
||||||
|
|
||||||
/// Group chat without a Group ID.
|
/// Group chat without a Group ID.
|
||||||
///
|
///
|
||||||
@@ -261,7 +261,7 @@ async fn get_to_and_past_contact_ids(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
|
ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
|
||||||
ChatAssignment::MailingList => None,
|
ChatAssignment::MailingListOrBroadcast => None,
|
||||||
ChatAssignment::OneOneChat => {
|
ChatAssignment::OneOneChat => {
|
||||||
if is_partial_download.is_none() && !mime_parser.incoming {
|
if is_partial_download.is_none() && !mime_parser.incoming {
|
||||||
parent_message.as_ref().map(|m| m.chat_id)
|
parent_message.as_ref().map(|m| m.chat_id)
|
||||||
@@ -326,7 +326,7 @@ async fn get_to_and_past_contact_ids(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatAssignment::Trash | ChatAssignment::MailingList => {
|
ChatAssignment::Trash | ChatAssignment::MailingListOrBroadcast => {
|
||||||
to_ids = Vec::new();
|
to_ids = Vec::new();
|
||||||
past_ids = Vec::new();
|
past_ids = Vec::new();
|
||||||
}
|
}
|
||||||
@@ -597,8 +597,8 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let prevent_rename =
|
let prevent_rename = (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
|
||||||
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
|
|| mime_parser.get_header(HeaderDef::Sender).is_some();
|
||||||
|
|
||||||
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
|
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
|
||||||
// the other To:/Cc: in the 3rd pass)
|
// the other To:/Cc: in the 3rd pass)
|
||||||
@@ -1201,6 +1201,8 @@ async fn decide_chat_assignment(
|
|||||||
|
|
||||||
let chat_assignment = if should_trash {
|
let chat_assignment = if should_trash {
|
||||||
ChatAssignment::Trash
|
ChatAssignment::Trash
|
||||||
|
} else if mime_parser.get_mailinglist_header().is_some() {
|
||||||
|
ChatAssignment::MailingListOrBroadcast
|
||||||
} else if let Some(grpid) = mime_parser.get_chat_group_id() {
|
} else if let Some(grpid) = mime_parser.get_chat_group_id() {
|
||||||
if mime_parser.was_encrypted() {
|
if mime_parser.was_encrypted() {
|
||||||
ChatAssignment::GroupChat {
|
ChatAssignment::GroupChat {
|
||||||
@@ -1228,8 +1230,6 @@ async fn decide_chat_assignment(
|
|||||||
// Group ID is ignored, however.
|
// Group ID is ignored, however.
|
||||||
ChatAssignment::AdHocGroup
|
ChatAssignment::AdHocGroup
|
||||||
}
|
}
|
||||||
} else if mime_parser.get_mailinglist_header().is_some() {
|
|
||||||
ChatAssignment::MailingList
|
|
||||||
} else if let Some(parent) = &parent_message {
|
} else if let Some(parent) = &parent_message {
|
||||||
if let Some((chat_id, chat_id_blocked)) =
|
if let Some((chat_id, chat_id_blocked)) =
|
||||||
lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
|
lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
|
||||||
@@ -1333,12 +1333,14 @@ async fn do_chat_assignment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatAssignment::MailingList => {
|
ChatAssignment::MailingListOrBroadcast => {
|
||||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
|
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||||
|
create_or_lookup_mailinglist_or_broadcast(
|
||||||
context,
|
context,
|
||||||
allow_creation,
|
allow_creation,
|
||||||
mailinglist_header,
|
mailinglist_header,
|
||||||
|
from_id,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -1380,7 +1382,7 @@ async fn do_chat_assignment(
|
|||||||
// unblock the chat
|
// unblock the chat
|
||||||
if chat_id_blocked != Blocked::Not
|
if chat_id_blocked != Blocked::Not
|
||||||
&& create_blocked != Blocked::Yes
|
&& create_blocked != Blocked::Yes
|
||||||
&& !matches!(chat_assignment, ChatAssignment::MailingList)
|
&& !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
|
||||||
{
|
{
|
||||||
if let Some(chat_id) = chat_id {
|
if let Some(chat_id) = chat_id {
|
||||||
chat_id.set_blocked(context, create_blocked).await?;
|
chat_id.set_blocked(context, create_blocked).await?;
|
||||||
@@ -1510,8 +1512,9 @@ async fn do_chat_assignment(
|
|||||||
chat_id = Some(*new_chat_id);
|
chat_id = Some(*new_chat_id);
|
||||||
chat_id_blocked = *new_chat_id_blocked;
|
chat_id_blocked = *new_chat_id_blocked;
|
||||||
}
|
}
|
||||||
ChatAssignment::MailingList => {
|
ChatAssignment::MailingListOrBroadcast => {
|
||||||
// Check if the message belongs to a broadcast list.
|
// Check if the message belongs to a broadcast channel
|
||||||
|
// (it can't be a mailing list, since it's outgoing)
|
||||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||||
let listid = mailinglist_header_listid(mailinglist_header)?;
|
let listid = mailinglist_header_listid(mailinglist_header)?;
|
||||||
chat_id = Some(
|
chat_id = Some(
|
||||||
@@ -1521,7 +1524,7 @@ async fn do_chat_assignment(
|
|||||||
} else {
|
} else {
|
||||||
let name =
|
let name =
|
||||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||||
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
|
chat::create_broadcast_ex(context, Nosync, listid, name).await?
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1662,16 +1665,28 @@ async fn add_parts(
|
|||||||
let is_location_kml = mime_parser.location_kml.is_some();
|
let is_location_kml = mime_parser.location_kml.is_some();
|
||||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||||
|
|
||||||
let mut group_changes = apply_group_changes(
|
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
|
let mut group_changes = match chat.typ {
|
||||||
|
_ if chat.id.is_special() => GroupChangesInfo::default(),
|
||||||
|
Chattype::Single => GroupChangesInfo::default(),
|
||||||
|
Chattype::Mailinglist => GroupChangesInfo::default(),
|
||||||
|
Chattype::OutBroadcast => GroupChangesInfo::default(),
|
||||||
|
Chattype::Group => {
|
||||||
|
apply_group_changes(
|
||||||
context,
|
context,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
chat_id,
|
&mut chat,
|
||||||
from_id,
|
from_id,
|
||||||
to_ids,
|
to_ids,
|
||||||
past_ids,
|
past_ids,
|
||||||
&verified_encryption,
|
&verified_encryption,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?
|
||||||
|
}
|
||||||
|
Chattype::InBroadcast => {
|
||||||
|
apply_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let rfc724_mid_orig = &mime_parser
|
let rfc724_mid_orig = &mime_parser
|
||||||
.get_rfc724_mid()
|
.get_rfc724_mid()
|
||||||
@@ -2771,21 +2786,15 @@ struct GroupChangesInfo {
|
|||||||
async fn apply_group_changes(
|
async fn apply_group_changes(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mime_parser: &mut MimeMessage,
|
mime_parser: &mut MimeMessage,
|
||||||
chat_id: ChatId,
|
chat: &mut Chat,
|
||||||
from_id: ContactId,
|
from_id: ContactId,
|
||||||
to_ids: &[Option<ContactId>],
|
to_ids: &[Option<ContactId>],
|
||||||
past_ids: &[Option<ContactId>],
|
past_ids: &[Option<ContactId>],
|
||||||
verified_encryption: &VerifiedEncryption,
|
verified_encryption: &VerifiedEncryption,
|
||||||
) -> Result<GroupChangesInfo> {
|
) -> Result<GroupChangesInfo> {
|
||||||
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
|
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
|
||||||
if chat_id.is_special() {
|
ensure!(chat.typ == Chattype::Group);
|
||||||
// Do not apply group changes to the trash chat.
|
ensure!(!chat.id.is_special());
|
||||||
return Ok(GroupChangesInfo::default());
|
|
||||||
}
|
|
||||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
|
||||||
if chat.typ != Chattype::Group {
|
|
||||||
return Ok(GroupChangesInfo::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut send_event_chat_modified = false;
|
let mut send_event_chat_modified = false;
|
||||||
let (mut removed_id, mut added_id) = (None, None);
|
let (mut removed_id, mut added_id) = (None, None);
|
||||||
@@ -2801,7 +2810,7 @@ async fn apply_group_changes(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let chat_contacts =
|
let chat_contacts =
|
||||||
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
|
||||||
let is_from_in_chat =
|
let is_from_in_chat =
|
||||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||||
|
|
||||||
@@ -2814,11 +2823,12 @@ async fn apply_group_changes(
|
|||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"Not marking chat {chat_id} as protected due to verification problem: {err:#}."
|
"Not marking chat {} as protected due to verification problem: {err:#}.",
|
||||||
|
chat.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if !chat.is_protected() {
|
} else if !chat.is_protected() {
|
||||||
chat_id
|
chat.id
|
||||||
.set_protection(
|
.set_protection(
|
||||||
context,
|
context,
|
||||||
ProtectionStatus::Protected,
|
ProtectionStatus::Protected,
|
||||||
@@ -2838,7 +2848,7 @@ async fn apply_group_changes(
|
|||||||
// rather than old display name.
|
// rather than old display name.
|
||||||
// This could be fixed by looking up the contact with the highest
|
// This could be fixed by looking up the contact with the highest
|
||||||
// `remove_timestamp` after applying Chat-Group-Member-Timestamps.
|
// `remove_timestamp` after applying Chat-Group-Member-Timestamps.
|
||||||
removed_id = lookup_key_contact_by_address(context, removed_addr, Some(chat_id)).await?;
|
removed_id = lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
|
||||||
if let Some(id) = removed_id {
|
if let Some(id) = removed_id {
|
||||||
better_msg = if id == from_id {
|
better_msg = if id == from_id {
|
||||||
silent = true;
|
silent = true;
|
||||||
@@ -2872,70 +2882,15 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let group_name_timestamp = mime_parser
|
apply_chat_name_and_avatar_changes(
|
||||||
.get_header(HeaderDef::ChatGroupNameTimestamp)
|
context,
|
||||||
.and_then(|s| s.parse::<i64>().ok());
|
mime_parser,
|
||||||
if let Some(old_name) = mime_parser
|
from_id,
|
||||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
chat,
|
||||||
.map(|s| s.trim())
|
&mut send_event_chat_modified,
|
||||||
.or(match group_name_timestamp {
|
&mut better_msg,
|
||||||
Some(0) => None,
|
)
|
||||||
Some(_) => Some(chat.name.as_str()),
|
|
||||||
None => None,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if let Some(grpname) = mime_parser
|
|
||||||
.get_header(HeaderDef::ChatGroupName)
|
|
||||||
.map(|grpname| grpname.trim())
|
|
||||||
.filter(|grpname| grpname.len() < 200)
|
|
||||||
{
|
|
||||||
let grpname = &sanitize_single_line(grpname);
|
|
||||||
|
|
||||||
let chat_group_name_timestamp =
|
|
||||||
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
|
||||||
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
|
||||||
// To provide group name consistency, compare names if timestamps are equal.
|
|
||||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
|
|
||||||
&& chat_id
|
|
||||||
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
|
||||||
.await?
|
|
||||||
&& grpname != &chat.name
|
|
||||||
{
|
|
||||||
info!(context, "Updating grpname for chat {chat_id}.");
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat_id))
|
|
||||||
.await?;
|
.await?;
|
||||||
send_event_chat_modified = true;
|
|
||||||
}
|
|
||||||
if mime_parser
|
|
||||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
let old_name = &sanitize_single_line(old_name);
|
|
||||||
better_msg.get_or_insert(
|
|
||||||
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
|
|
||||||
if value == "group-avatar-changed" {
|
|
||||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
|
||||||
// this is just an explicit message containing the group-avatar,
|
|
||||||
// apart from that, the group-avatar is send along with various other messages
|
|
||||||
better_msg = match avatar_action {
|
|
||||||
AvatarAction::Delete => {
|
|
||||||
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
|
|
||||||
}
|
|
||||||
AvatarAction::Change(_) => {
|
|
||||||
Some(stock_str::msg_grp_img_changed(context, from_id).await)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_from_in_chat {
|
if is_from_in_chat {
|
||||||
if chat.member_list_is_stale(context).await? {
|
if chat.member_list_is_stale(context).await? {
|
||||||
@@ -2954,7 +2909,7 @@ async fn apply_group_changes(
|
|||||||
transaction.execute(
|
transaction.execute(
|
||||||
"DELETE FROM chats_contacts
|
"DELETE FROM chats_contacts
|
||||||
WHERE chat_id=?",
|
WHERE chat_id=?",
|
||||||
(chat_id,),
|
(chat.id,),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Insert contacts with default timestamps of 0.
|
// Insert contacts with default timestamps of 0.
|
||||||
@@ -2963,7 +2918,7 @@ async fn apply_group_changes(
|
|||||||
VALUES (?, ?)",
|
VALUES (?, ?)",
|
||||||
)?;
|
)?;
|
||||||
for contact_id in &new_members {
|
for contact_id in &new_members {
|
||||||
statement.execute((chat_id, contact_id))?;
|
statement.execute((chat.id, contact_id))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2975,7 +2930,7 @@ async fn apply_group_changes(
|
|||||||
{
|
{
|
||||||
send_event_chat_modified |= update_chats_contacts_timestamps(
|
send_event_chat_modified |= update_chats_contacts_timestamps(
|
||||||
context,
|
context,
|
||||||
chat_id,
|
chat.id,
|
||||||
Some(from_id),
|
Some(from_id),
|
||||||
to_ids,
|
to_ids,
|
||||||
past_ids,
|
past_ids,
|
||||||
@@ -3015,7 +2970,7 @@ async fn apply_group_changes(
|
|||||||
chat::update_chat_contacts_table(
|
chat::update_chat_contacts_table(
|
||||||
context,
|
context,
|
||||||
mime_parser.timestamp_sent,
|
mime_parser.timestamp_sent,
|
||||||
chat_id,
|
chat.id,
|
||||||
&new_members,
|
&new_members,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -3023,7 +2978,7 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chat_id
|
chat.id
|
||||||
.update_timestamp(
|
.update_timestamp(
|
||||||
context,
|
context,
|
||||||
Param::MemberListTimestamp,
|
Param::MemberListTimestamp,
|
||||||
@@ -3033,7 +2988,7 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
||||||
chat::get_chat_contacts(context, chat_id)
|
chat::get_chat_contacts(context, chat.id)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.copied(),
|
.copied(),
|
||||||
@@ -3063,22 +3018,108 @@ async fn apply_group_changes(
|
|||||||
let group_changes_msgs = if self_added {
|
let group_changes_msgs = if self_added {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
group_changes_msgs(context, &added_ids, &removed_ids, chat_id).await?
|
group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
if send_event_chat_modified {
|
||||||
if !new_chat_contacts.contains(&ContactId::SELF) {
|
context.emit_event(EventType::ChatModified(chat.id));
|
||||||
warn!(
|
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||||
context,
|
}
|
||||||
"Received group avatar update for group chat {chat_id} we are not a member of."
|
Ok(GroupChangesInfo {
|
||||||
);
|
better_msg,
|
||||||
} else if !new_chat_contacts.contains(&from_id) {
|
added_removed_id: if added_id.is_some() {
|
||||||
warn!(
|
added_id
|
||||||
context,
|
|
||||||
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
info!(context, "Group-avatar change for {chat_id}.");
|
removed_id
|
||||||
|
},
|
||||||
|
silent,
|
||||||
|
extra_msgs: group_changes_msgs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies incoming changes to the group's or broadcast channel's name and avatar.
|
||||||
|
///
|
||||||
|
/// - `send_event_chat_modified` is set to `true` if ChatModified event should be sent
|
||||||
|
/// - `better_msg` is filled with an info message about name change, if necessary
|
||||||
|
async fn apply_chat_name_and_avatar_changes(
|
||||||
|
context: &Context,
|
||||||
|
mime_parser: &MimeMessage,
|
||||||
|
from_id: ContactId,
|
||||||
|
chat: &mut Chat,
|
||||||
|
send_event_chat_modified: &mut bool,
|
||||||
|
better_msg: &mut Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// ========== Apply chat name changes ==========
|
||||||
|
|
||||||
|
let group_name_timestamp = mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupNameTimestamp)
|
||||||
|
.and_then(|s| s.parse::<i64>().ok());
|
||||||
|
|
||||||
|
if let Some(old_name) = mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.or(match group_name_timestamp {
|
||||||
|
Some(0) => None,
|
||||||
|
Some(_) => Some(chat.name.as_str()),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if let Some(grpname) = mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupName)
|
||||||
|
.map(|grpname| grpname.trim())
|
||||||
|
.filter(|grpname| grpname.len() < 200)
|
||||||
|
{
|
||||||
|
let grpname = &sanitize_single_line(grpname);
|
||||||
|
|
||||||
|
let chat_group_name_timestamp =
|
||||||
|
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
||||||
|
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
||||||
|
// To provide group name consistency, compare names if timestamps are equal.
|
||||||
|
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
|
||||||
|
&& chat
|
||||||
|
.id
|
||||||
|
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
||||||
|
.await?
|
||||||
|
&& grpname != &chat.name
|
||||||
|
{
|
||||||
|
info!(context, "Updating grpname for chat {}.", chat.id);
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
|
||||||
|
.await?;
|
||||||
|
*send_event_chat_modified = true;
|
||||||
|
}
|
||||||
|
if mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
let old_name = &sanitize_single_line(old_name);
|
||||||
|
better_msg.get_or_insert(
|
||||||
|
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Apply chat avatar changes ==========
|
||||||
|
|
||||||
|
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
|
||||||
|
if value == "group-avatar-changed" {
|
||||||
|
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||||
|
// this is just an explicit message containing the group-avatar,
|
||||||
|
// apart from that, the group-avatar is send along with various other messages
|
||||||
|
better_msg.get_or_insert(match avatar_action {
|
||||||
|
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||||
|
AvatarAction::Change(_) => {
|
||||||
|
stock_str::msg_grp_img_changed(context, from_id).await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||||
|
info!(context, "Group-avatar change for {}.", chat.id);
|
||||||
if chat
|
if chat
|
||||||
.param
|
.param
|
||||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||||
@@ -3092,25 +3133,11 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
chat.update_param(context).await?;
|
chat.update_param(context).await?;
|
||||||
send_event_chat_modified = true;
|
*send_event_chat_modified = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if send_event_chat_modified {
|
Ok(())
|
||||||
context.emit_event(EventType::ChatModified(chat_id));
|
|
||||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
|
||||||
}
|
|
||||||
Ok(GroupChangesInfo {
|
|
||||||
better_msg,
|
|
||||||
added_removed_id: if added_id.is_some() {
|
|
||||||
added_id
|
|
||||||
} else {
|
|
||||||
removed_id
|
|
||||||
},
|
|
||||||
silent,
|
|
||||||
extra_msgs: group_changes_msgs,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
|
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
|
||||||
@@ -3165,7 +3192,7 @@ fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
|||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create or lookup a mailing list chat.
|
/// Create or lookup a mailing list or incoming broadcast channel chat.
|
||||||
///
|
///
|
||||||
/// `list_id_header` contains the Id that must be used for the mailing list
|
/// `list_id_header` contains the Id that must be used for the mailing list
|
||||||
/// and has the form `Name <Id>`, `<Id>` or just `Id`.
|
/// and has the form `Name <Id>`, `<Id>` or just `Id`.
|
||||||
@@ -3174,10 +3201,11 @@ fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
|||||||
///
|
///
|
||||||
/// `mime_parser` is the corresponding message
|
/// `mime_parser` is the corresponding message
|
||||||
/// and is used to figure out the mailing list name from different header fields.
|
/// and is used to figure out the mailing list name from different header fields.
|
||||||
async fn create_or_lookup_mailinglist(
|
async fn create_or_lookup_mailinglist_or_broadcast(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
allow_creation: bool,
|
allow_creation: bool,
|
||||||
list_id_header: &str,
|
list_id_header: &str,
|
||||||
|
from_id: ContactId,
|
||||||
mime_parser: &MimeMessage,
|
mime_parser: &MimeMessage,
|
||||||
) -> Result<Option<(ChatId, Blocked)>> {
|
) -> Result<Option<(ChatId, Blocked)>> {
|
||||||
let listid = mailinglist_header_listid(list_id_header)?;
|
let listid = mailinglist_header_listid(list_id_header)?;
|
||||||
@@ -3186,7 +3214,19 @@ async fn create_or_lookup_mailinglist(
|
|||||||
return Ok(Some((chat_id, blocked)));
|
return Ok(Some((chat_id, blocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
|
let chattype = if mime_parser.was_encrypted() {
|
||||||
|
Chattype::InBroadcast
|
||||||
|
} else {
|
||||||
|
Chattype::Mailinglist
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = if chattype == Chattype::InBroadcast {
|
||||||
|
mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupName)
|
||||||
|
.unwrap_or("Broadcast Channel")
|
||||||
|
} else {
|
||||||
|
&compute_mailinglist_name(list_id_header, &listid, mime_parser)
|
||||||
|
};
|
||||||
|
|
||||||
if allow_creation {
|
if allow_creation {
|
||||||
// list does not exist but should be created
|
// list does not exist but should be created
|
||||||
@@ -3204,9 +3244,9 @@ async fn create_or_lookup_mailinglist(
|
|||||||
};
|
};
|
||||||
let chat_id = ChatId::create_multiuser_record(
|
let chat_id = ChatId::create_multiuser_record(
|
||||||
context,
|
context,
|
||||||
Chattype::Mailinglist,
|
chattype,
|
||||||
&listid,
|
&listid,
|
||||||
&name,
|
name,
|
||||||
blocked,
|
blocked,
|
||||||
ProtectionStatus::Unprotected,
|
ProtectionStatus::Unprotected,
|
||||||
param,
|
param,
|
||||||
@@ -3227,6 +3267,15 @@ async fn create_or_lookup_mailinglist(
|
|||||||
&[ContactId::SELF],
|
&[ContactId::SELF],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
if chattype == Chattype::InBroadcast {
|
||||||
|
chat::add_to_chat_contacts_table(
|
||||||
|
context,
|
||||||
|
mime_parser.timestamp_sent,
|
||||||
|
chat_id,
|
||||||
|
&[from_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
Ok(Some((chat_id, blocked)))
|
Ok(Some((chat_id, blocked)))
|
||||||
} else {
|
} else {
|
||||||
info!(context, "Creating list forbidden by caller.");
|
info!(context, "Creating list forbidden by caller.");
|
||||||
@@ -3372,6 +3421,39 @@ async fn apply_mailinglist_changes(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn apply_broadcast_changes(
|
||||||
|
context: &Context,
|
||||||
|
mime_parser: &MimeMessage,
|
||||||
|
chat: &mut Chat,
|
||||||
|
from_id: ContactId,
|
||||||
|
) -> Result<GroupChangesInfo> {
|
||||||
|
ensure!(chat.typ == Chattype::InBroadcast);
|
||||||
|
|
||||||
|
let mut send_event_chat_modified = false;
|
||||||
|
let mut better_msg = None;
|
||||||
|
|
||||||
|
apply_chat_name_and_avatar_changes(
|
||||||
|
context,
|
||||||
|
mime_parser,
|
||||||
|
from_id,
|
||||||
|
chat,
|
||||||
|
&mut send_event_chat_modified,
|
||||||
|
&mut better_msg,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if send_event_chat_modified {
|
||||||
|
context.emit_event(EventType::ChatModified(chat.id));
|
||||||
|
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||||
|
}
|
||||||
|
Ok(GroupChangesInfo {
|
||||||
|
better_msg,
|
||||||
|
added_removed_id: None,
|
||||||
|
silent: false,
|
||||||
|
extra_msgs: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates ad-hoc group and returns chat ID on success.
|
/// Creates ad-hoc group and returns chat ID on success.
|
||||||
async fn create_adhoc_group(
|
async fn create_adhoc_group(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
|
|||||||
@@ -1711,7 +1711,7 @@ fn migrate_key_contacts(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast list
|
// Broadcast channel
|
||||||
160 => old_members
|
160 => old_members
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(original, _)| {
|
.map(|(original, _)| {
|
||||||
|
|||||||
@@ -237,9 +237,6 @@ pub enum StockMessage {
|
|||||||
#[strum(props(fallback = "Messages"))]
|
#[strum(props(fallback = "Messages"))]
|
||||||
Messages = 114,
|
Messages = 114,
|
||||||
|
|
||||||
#[strum(props(fallback = "Broadcast List"))]
|
|
||||||
BroadcastList = 115,
|
|
||||||
|
|
||||||
#[strum(props(fallback = "%1$s of %2$s used"))]
|
#[strum(props(fallback = "%1$s of %2$s used"))]
|
||||||
PartOfTotallUsed = 116,
|
PartOfTotallUsed = 116,
|
||||||
|
|
||||||
@@ -1221,12 +1218,6 @@ pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &st
|
|||||||
.replace2(total)
|
.replace2(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stock string: `Broadcast List`.
|
|
||||||
/// Used as the default name for broadcast lists; a number may be added.
|
|
||||||
pub(crate) async fn broadcast_list(context: &Context) -> String {
|
|
||||||
translated(context, StockMessage::BroadcastList).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
|
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
|
||||||
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
|
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
|
||||||
translated(context, StockMessage::InvalidUnencryptedMail)
|
translated(context, StockMessage::InvalidUnencryptedMail)
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ impl Summary {
|
|||||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||||
}
|
}
|
||||||
} else if chat.typ == Chattype::Group
|
} else if chat.typ == Chattype::Group
|
||||||
|| chat.typ == Chattype::Broadcast
|
|| chat.typ == Chattype::OutBroadcast
|
||||||
|
|| chat.typ == Chattype::InBroadcast
|
||||||
|| chat.typ == Chattype::Mailinglist
|
|| chat.typ == Chattype::Mailinglist
|
||||||
|| chat.is_self_talk()
|
|| chat.is_self_talk()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ use crate::tools::time;
|
|||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
pub const AVATAR_64x64_BYTES: &[u8] = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
|
|
||||||
|
/// The filename of [`AVATAR_64x64_BYTES`],
|
||||||
|
/// after it has been saved
|
||||||
|
/// by [`crate::blob::BlobObject::create_and_deduplicate`].
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
pub const AVATAR_64x64_DEDUPLICATED: &str = "e9b6c7a78aa2e4f415644f55a553e73.png";
|
||||||
|
|
||||||
/// Map of context IDs to names for [`TestContext`]s.
|
/// Map of context IDs to names for [`TestContext`]s.
|
||||||
static CONTEXT_NAMES: LazyLock<std::sync::RwLock<BTreeMap<u32, String>>> =
|
static CONTEXT_NAMES: LazyLock<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||||
LazyLock::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
LazyLock::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
||||||
@@ -1147,7 +1156,7 @@ impl Drop for InnerLogSink {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SentMessage<'a> {
|
pub struct SentMessage<'a> {
|
||||||
pub payload: String,
|
pub payload: String,
|
||||||
recipients: String,
|
pub recipients: String,
|
||||||
pub sender_msg_id: MsgId,
|
pub sender_msg_id: MsgId,
|
||||||
sender_context: &'a Context,
|
sender_context: &'a Context,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde_json::json;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::{
|
use crate::chat::{
|
||||||
ChatId, ProtectionStatus, add_contact_to_chat, create_broadcast_list, create_group_chat,
|
ChatId, ProtectionStatus, add_contact_to_chat, create_broadcast, create_group_chat,
|
||||||
forward_msgs, remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
|
forward_msgs, remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
|
||||||
};
|
};
|
||||||
use crate::chatlist::Chatlist;
|
use crate::chatlist::Chatlist;
|
||||||
@@ -1621,7 +1621,7 @@ async fn test_webxdc_no_internet_access() -> Result<()> {
|
|||||||
let self_id = t.get_self_chat().await.id;
|
let self_id = t.get_self_chat().await.id;
|
||||||
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
|
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
|
||||||
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
||||||
let broadcast_id = create_broadcast_list(&t).await?;
|
let broadcast_id = create_broadcast(&t, "Channel".to_string()).await?;
|
||||||
|
|
||||||
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
||||||
for internet_xdc in [true, false] {
|
for internet_xdc in [true, false] {
|
||||||
|
|||||||
Reference in New Issue
Block a user