mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +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
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(chat::create_broadcast_list(ctx))
|
||||
.context("Failed to create broadcast list")
|
||||
block_on(chat::create_broadcast(ctx, "Channel".to_string()))
|
||||
.context("Failed to create broadcast channel")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
|
||||
@@ -926,7 +926,7 @@ impl CommandApi {
|
||||
/// explicitly as it may happen that oneself gets removed from a still existing
|
||||
/// 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 now, the UI should not show the list for mailing lists.
|
||||
@@ -975,18 +975,30 @@ impl CommandApi {
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new broadcast list.
|
||||
///
|
||||
/// 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.
|
||||
/// Deprecated 2025-07 in favor of create_broadcast().
|
||||
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?;
|
||||
chat::create_broadcast_list(&ctx)
|
||||
chat::create_broadcast(&ctx, chat_name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
@@ -64,8 +64,10 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// true when chat is a broadcastlist
|
||||
/// Deprecated 2025-07, alias for is_out_broadcast
|
||||
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)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
@@ -172,7 +174,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
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,
|
||||
was_seen_recently,
|
||||
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.");
|
||||
}
|
||||
"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.");
|
||||
}
|
||||
|
||||
@@ -288,10 +288,46 @@ class Account:
|
||||
def create_group(self, name: str, protect: bool = False) -> 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))
|
||||
|
||||
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:
|
||||
"""Return the Chat instance with the given ID."""
|
||||
return Chat(self, chat_id)
|
||||
|
||||
@@ -90,10 +90,40 @@ class ChatType(IntEnum):
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
|
||||
SINGLE = 100
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = 120
|
||||
|
||||
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):
|
||||
|
||||
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -846,3 +846,36 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
assert msg.text == "hello"
|
||||
|
||||
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::param::Param;
|
||||
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;
|
||||
|
||||
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() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
||||
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
|
||||
assert!(!avatar_blob.exists());
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
@@ -251,7 +250,7 @@ async fn test_selfavatar_copy_without_recode() {
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
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();
|
||||
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.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
/// Incoming broadcast channel where the user can't send messages.
|
||||
InBroadcast,
|
||||
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
|
||||
@@ -146,6 +149,9 @@ impl fmt::Display for CantSendReason {
|
||||
Self::ReadOnlyMailingList => {
|
||||
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::MissingKey => write!(f, "key is missing"),
|
||||
}
|
||||
@@ -395,7 +401,7 @@ impl ChatId {
|
||||
let mut delete = false;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Broadcast => {
|
||||
Chattype::OutBroadcast => {
|
||||
bail!("Can't block chat of type {:?}", chat.typ)
|
||||
}
|
||||
Chattype::Single => {
|
||||
@@ -413,7 +419,7 @@ impl ChatId {
|
||||
info!(context, "Can't block groups yet, deleting the chat.");
|
||||
delete = true;
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
Chattype::Mailinglist | Chattype::InBroadcast => {
|
||||
if self.set_blocked(context, Blocked::Yes).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
}
|
||||
@@ -479,7 +485,7 @@ impl ChatId {
|
||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
}
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||
@@ -529,7 +535,10 @@ impl ChatId {
|
||||
|
||||
match protect {
|
||||
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"),
|
||||
},
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
@@ -1659,6 +1668,12 @@ impl Chat {
|
||||
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.
|
||||
let reason = NotAMember;
|
||||
@@ -1692,8 +1707,9 @@ impl Chat {
|
||||
/// 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> {
|
||||
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::InBroadcast => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1758,8 +1774,6 @@ impl Chat {
|
||||
if !image_rel.is_empty() {
|
||||
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)
|
||||
}
|
||||
@@ -1872,7 +1886,7 @@ impl Chat {
|
||||
!self.grpid.is_empty()
|
||||
}
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::Broadcast => true,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => true,
|
||||
};
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
@@ -1970,7 +1984,7 @@ impl Chat {
|
||||
);
|
||||
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
|
||||
{
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
@@ -2291,7 +2305,10 @@ impl Chat {
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
|
||||
Chattype::OutBroadcast
|
||||
| Chattype::InBroadcast
|
||||
| Chattype::Group
|
||||
| Chattype::Mailinglist => {
|
||||
if !self.grpid.is_empty() {
|
||||
return Ok(Some(SyncId::Grpid(self.grpid.clone())));
|
||||
}
|
||||
@@ -2465,15 +2482,6 @@ pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
|
||||
.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> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
@@ -3614,37 +3622,27 @@ pub async fn create_group_chat(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Finds an unused name for a new broadcast list.
|
||||
async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
let base_name = stock_str::broadcast_list(context).await;
|
||||
for attempt in 1..1000 {
|
||||
let better_name = if attempt > 1 {
|
||||
format!("{base_name} {attempt}")
|
||||
} else {
|
||||
base_name.clone()
|
||||
};
|
||||
if !context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
|
||||
(Chattype::Broadcast, &better_name),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
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?;
|
||||
/// 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_chat`] for more information on the unpromoted state.
|
||||
///
|
||||
/// Returns the created chat's id.
|
||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||
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,
|
||||
sync: sync::Sync,
|
||||
grpid: String,
|
||||
@@ -3659,7 +3657,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
if cnt == 1 {
|
||||
return Ok(t.query_row(
|
||||
"SELECT id FROM chats WHERE grpid=? AND type=?",
|
||||
(grpid, Chattype::Broadcast),
|
||||
(grpid, Chattype::OutBroadcast),
|
||||
|row| {
|
||||
let id: isize = row.get(0)?;
|
||||
Ok(id)
|
||||
@@ -3671,7 +3669,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
(type, name, grpid, param, created_timestamp) \
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
Chattype::OutBroadcast,
|
||||
&chat_name,
|
||||
&grpid,
|
||||
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
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
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",
|
||||
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.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast."
|
||||
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast channel."
|
||||
);
|
||||
ensure!(
|
||||
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 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? {
|
||||
let err_msg = format!(
|
||||
"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
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
{
|
||||
if chat.name == new_name {
|
||||
success = true;
|
||||
@@ -4166,7 +4164,6 @@ async fn rename_ex(
|
||||
.await?;
|
||||
if chat.is_promoted()
|
||||
&& !chat.is_mailing_list()
|
||||
&& chat.typ != Chattype::Broadcast
|
||||
&& sanitize_single_line(&chat.name) != new_name
|
||||
{
|
||||
msg.viewtype = Viewtype::Text;
|
||||
@@ -4212,15 +4209,15 @@ pub async fn set_chat_profile_image(
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group,
|
||||
"Can only set profile image for group chats"
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"Can only set profile image for groups / broadcasts"
|
||||
);
|
||||
ensure!(
|
||||
!chat.grpid.is_empty(),
|
||||
"Cannot set profile image for ad hoc groups"
|
||||
);
|
||||
/* 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(
|
||||
"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.");
|
||||
}
|
||||
|
||||
// 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 {
|
||||
msg.param
|
||||
.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}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
chat.typ == Chattype::OutBroadcast,
|
||||
"{id} is not a broadcast list",
|
||||
);
|
||||
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,))?;
|
||||
|
||||
// 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
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
for contact_id in &contacts {
|
||||
@@ -4835,7 +4828,7 @@ async fn set_contacts_by_fingerprints(
|
||||
"Cannot add key-contacts to unencrypted chat {id}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
chat.typ == Chattype::OutBroadcast,
|
||||
"{id} is not a broadcast list",
|
||||
);
|
||||
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,))?;
|
||||
|
||||
// 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
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
for contact_id in &contacts {
|
||||
@@ -4895,7 +4888,7 @@ pub(crate) enum SyncAction {
|
||||
Accept,
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(MuteDuration),
|
||||
/// Create broadcast list with the given name.
|
||||
/// Create broadcast channel with the given name.
|
||||
CreateBroadcast(String),
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
@@ -4960,7 +4953,7 @@ impl Context {
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
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(());
|
||||
}
|
||||
get_chat_id_by_grpid(self, grpid)
|
||||
|
||||
@@ -6,7 +6,10 @@ use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
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 strum::IntoEnumIterator;
|
||||
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 group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").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?;
|
||||
for chat_id in &[single_id, group_id, broadcast_id] {
|
||||
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;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// test broadcast list
|
||||
let broadcast_id = create_broadcast_list(&alice).await?;
|
||||
// test broadcast channel
|
||||
let broadcast_id = create_broadcast(&alice, "Channel".to_string()).await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
broadcast_id,
|
||||
@@ -2629,11 +2632,11 @@ async fn test_broadcast() -> Result<()> {
|
||||
.await?;
|
||||
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).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?;
|
||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||
assert_eq!(chat.name, "Broadcast list");
|
||||
assert_eq!(chat.typ, Chattype::OutBroadcast);
|
||||
assert_eq!(chat.name, "Broadcast channel");
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
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));
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
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_override_sender_name().is_none());
|
||||
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_eq!(chat.name, "Broadcast list");
|
||||
assert_eq!(chat.name, "Broadcast channel");
|
||||
assert!(!chat.is_self_talk());
|
||||
}
|
||||
|
||||
@@ -2672,6 +2676,14 @@ async fn test_broadcast() -> Result<()> {
|
||||
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)]
|
||||
async fn test_broadcast_multidev() -> Result<()> {
|
||||
let alices = [
|
||||
@@ -2681,9 +2693,9 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
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?;
|
||||
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 msg = alices[1].recv_msg(&sent_msg).await;
|
||||
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;
|
||||
assert_eq!(msg.chat_id, a1_broadcast_id);
|
||||
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_name(), "Broadcast list 42");
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
assert!(
|
||||
get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||
.await?
|
||||
@@ -2701,13 +2713,13 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
);
|
||||
|
||||
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 msg = alices[0].recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
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_name(), "Broadcast list 42");
|
||||
assert_eq!(a0_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
assert!(
|
||||
get_chat_contacts(&alices[0], a0_broadcast_id)
|
||||
.await?
|
||||
@@ -2717,6 +2729,161 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
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)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -3403,6 +3570,7 @@ async fn test_sync_muted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that synchronizing broadcast channels via sync-messages works
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3414,7 +3582,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
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;
|
||||
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)
|
||||
@@ -3422,7 +3590,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
.unwrap()
|
||||
.0;
|
||||
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!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
||||
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 msg = bob.recv_msg(&sent_msg).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;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
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] {
|
||||
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;
|
||||
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;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
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_name(), "Broadcast list 42");
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,8 @@ impl Chatlist {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
|
||||
@@ -126,17 +126,46 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
)]
|
||||
#[repr(u32)]
|
||||
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,
|
||||
|
||||
/// Group chat.
|
||||
///
|
||||
/// Created by [`crate::chat::create_group_chat`].
|
||||
Group = 120,
|
||||
|
||||
/// Mailing list.
|
||||
/// An (unencrypted) mailing list,
|
||||
/// created by an incoming mailing list email.
|
||||
Mailinglist = 140,
|
||||
|
||||
/// Broadcast list.
|
||||
Broadcast = 160,
|
||||
/// Outgoing broadcast channel, called "Channel" in the UI.
|
||||
///
|
||||
/// 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;
|
||||
@@ -239,7 +268,7 @@ mod tests {
|
||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).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]
|
||||
|
||||
@@ -1179,7 +1179,7 @@ impl Contact {
|
||||
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
|
||||
/// (this way, only one unblock-ffi is needed and only one set of ui-functions,
|
||||
/// from the users perspective,
|
||||
@@ -1188,15 +1188,20 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
|
||||
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
|
||||
let name: String = row.get(0)?;
|
||||
let grpid: String = row.get(1)?;
|
||||
Ok((name, grpid))
|
||||
})?;
|
||||
let mut stmt = transaction.prepare(
|
||||
"SELECT name, grpid, type FROM chats WHERE (type=? OR type=?) AND blocked=?",
|
||||
)?;
|
||||
let rows = stmt.query_map(
|
||||
(Chattype::Mailinglist, Chattype::InBroadcast, Blocked::Yes),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let grpid: String = row.get(1)?;
|
||||
let typ: Chattype = row.get(2)?;
|
||||
Ok((name, grpid, typ))
|
||||
},
|
||||
)?;
|
||||
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(
|
||||
"SELECT COUNT(id) FROM contacts WHERE addr=?",
|
||||
[&grpid],
|
||||
@@ -1209,10 +1214,17 @@ impl Contact {
|
||||
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.
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
|
||||
(&name, Origin::MailinglistAddress, &grpid),
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
|
||||
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -66,7 +66,7 @@ mod test_chatlist_events {
|
||||
use crate::{
|
||||
EventType,
|
||||
chat::{
|
||||
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast_list,
|
||||
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
|
||||
create_group_chat, set_muted,
|
||||
},
|
||||
config::Config,
|
||||
@@ -308,13 +308,13 @@ mod test_chatlist_events {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create broadcastlist
|
||||
/// Create broadcast channel
|
||||
#[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 alice = tcm.alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
create_broadcast_list(&alice).await?;
|
||||
create_broadcast(&alice, "Channel".to_string()).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -855,9 +855,10 @@ impl Message {
|
||||
|
||||
let contact = if self.from_id != ContactId::SELF {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
||||
}
|
||||
Chattype::Group
|
||||
| Chattype::OutBroadcast
|
||||
| Chattype::InBroadcast
|
||||
| Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
|
||||
Chattype::Single => None,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct MimeFactory {
|
||||
/// because in case of "member removed" message
|
||||
/// removed member is in the recipient list,
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// and encrypted message cannot be sent to them.
|
||||
to: Vec<(String, String)>,
|
||||
@@ -178,7 +178,7 @@ impl MimeFactory {
|
||||
let now = time();
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
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 config_displayname = context
|
||||
@@ -599,7 +599,7 @@ impl MimeFactory {
|
||||
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()
|
||||
{
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
@@ -791,7 +791,7 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Broadcast {
|
||||
if chat.typ == Chattype::OutBroadcast {
|
||||
headers.push((
|
||||
"List-ID",
|
||||
mail_builder::headers::text::Text::new(format!(
|
||||
@@ -1035,7 +1035,7 @@ impl MimeFactory {
|
||||
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if chat.typ != Chattype::Broadcast {
|
||||
if chat.typ != Chattype::OutBroadcast {
|
||||
for (addr, key) in &encryption_keys {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = msg.param.get_cmd();
|
||||
@@ -1300,9 +1300,9 @@ impl MimeFactory {
|
||||
let send_verified_headers = match chat.typ {
|
||||
Chattype::Single => 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::Broadcast => false,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => false,
|
||||
};
|
||||
if chat.is_protected() && send_verified_headers {
|
||||
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.
|
||||
if !chat.grpid.is_empty() {
|
||||
headers.push((
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::iter;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::{
|
||||
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
|
||||
@@ -93,10 +93,10 @@ enum ChatAssignment {
|
||||
/// assign to encrypted group.
|
||||
GroupChat { grpid: String },
|
||||
|
||||
/// Mailing list or broadcast list.
|
||||
/// Mailing list or broadcast channel.
|
||||
///
|
||||
/// Mailing lists don't have members.
|
||||
/// Broadcast lists have members
|
||||
/// Broadcast channels have members
|
||||
/// on the sender side,
|
||||
/// but their addresses don't go into
|
||||
/// the `To` field.
|
||||
@@ -107,7 +107,7 @@ enum ChatAssignment {
|
||||
/// up except the `from_id`
|
||||
/// which may be an email address contact
|
||||
/// or a key-contact.
|
||||
MailingList,
|
||||
MailingListOrBroadcast,
|
||||
|
||||
/// Group chat without a Group ID.
|
||||
///
|
||||
@@ -261,7 +261,7 @@ async fn get_to_and_past_contact_ids(
|
||||
None
|
||||
}
|
||||
ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
|
||||
ChatAssignment::MailingList => None,
|
||||
ChatAssignment::MailingListOrBroadcast => None,
|
||||
ChatAssignment::OneOneChat => {
|
||||
if is_partial_download.is_none() && !mime_parser.incoming {
|
||||
parent_message.as_ref().map(|m| m.chat_id)
|
||||
@@ -326,7 +326,7 @@ async fn get_to_and_past_contact_ids(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
ChatAssignment::Trash | ChatAssignment::MailingList => {
|
||||
ChatAssignment::Trash | ChatAssignment::MailingListOrBroadcast => {
|
||||
to_ids = Vec::new();
|
||||
past_ids = Vec::new();
|
||||
}
|
||||
@@ -597,8 +597,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let prevent_rename =
|
||||
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
|
||||
let prevent_rename = (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
|
||||
|| 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
|
||||
// the other To:/Cc: in the 3rd pass)
|
||||
@@ -1201,6 +1201,8 @@ async fn decide_chat_assignment(
|
||||
|
||||
let chat_assignment = if should_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() {
|
||||
if mime_parser.was_encrypted() {
|
||||
ChatAssignment::GroupChat {
|
||||
@@ -1228,8 +1230,6 @@ async fn decide_chat_assignment(
|
||||
// Group ID is ignored, however.
|
||||
ChatAssignment::AdHocGroup
|
||||
}
|
||||
} else if mime_parser.get_mailinglist_header().is_some() {
|
||||
ChatAssignment::MailingList
|
||||
} else if let Some(parent) = &parent_message {
|
||||
if let Some((chat_id, chat_id_blocked)) =
|
||||
lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
|
||||
@@ -1333,15 +1333,17 @@ async fn do_chat_assignment(
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatAssignment::MailingList => {
|
||||
ChatAssignment::MailingListOrBroadcast => {
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
|
||||
context,
|
||||
allow_creation,
|
||||
mailinglist_header,
|
||||
mime_parser,
|
||||
)
|
||||
.await?
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
create_or_lookup_mailinglist_or_broadcast(
|
||||
context,
|
||||
allow_creation,
|
||||
mailinglist_header,
|
||||
from_id,
|
||||
mime_parser,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
@@ -1380,7 +1382,7 @@ async fn do_chat_assignment(
|
||||
// unblock the chat
|
||||
if chat_id_blocked != Blocked::Not
|
||||
&& create_blocked != Blocked::Yes
|
||||
&& !matches!(chat_assignment, ChatAssignment::MailingList)
|
||||
&& !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
|
||||
{
|
||||
if let Some(chat_id) = chat_id {
|
||||
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_blocked = *new_chat_id_blocked;
|
||||
}
|
||||
ChatAssignment::MailingList => {
|
||||
// Check if the message belongs to a broadcast list.
|
||||
ChatAssignment::MailingListOrBroadcast => {
|
||||
// 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() {
|
||||
let listid = mailinglist_header_listid(mailinglist_header)?;
|
||||
chat_id = Some(
|
||||
@@ -1521,7 +1524,7 @@ async fn do_chat_assignment(
|
||||
} else {
|
||||
let name =
|
||||
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_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
let mut group_changes = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
&verified_encryption,
|
||||
)
|
||||
.await?;
|
||||
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,
|
||||
mime_parser,
|
||||
&mut chat,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
&verified_encryption,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Chattype::InBroadcast => {
|
||||
apply_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
|
||||
}
|
||||
};
|
||||
|
||||
let rfc724_mid_orig = &mime_parser
|
||||
.get_rfc724_mid()
|
||||
@@ -2771,21 +2786,15 @@ struct GroupChangesInfo {
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
chat_id: ChatId,
|
||||
chat: &mut Chat,
|
||||
from_id: ContactId,
|
||||
to_ids: &[Option<ContactId>],
|
||||
past_ids: &[Option<ContactId>],
|
||||
verified_encryption: &VerifiedEncryption,
|
||||
) -> Result<GroupChangesInfo> {
|
||||
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
|
||||
if chat_id.is_special() {
|
||||
// Do not apply group changes to the trash chat.
|
||||
return Ok(GroupChangesInfo::default());
|
||||
}
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
return Ok(GroupChangesInfo::default());
|
||||
}
|
||||
ensure!(chat.typ == Chattype::Group);
|
||||
ensure!(!chat.id.is_special());
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
@@ -2801,7 +2810,7 @@ async fn apply_group_changes(
|
||||
};
|
||||
|
||||
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 =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
|
||||
@@ -2814,11 +2823,12 @@ async fn apply_group_changes(
|
||||
} else {
|
||||
warn!(
|
||||
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() {
|
||||
chat_id
|
||||
chat.id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
@@ -2838,7 +2848,7 @@ async fn apply_group_changes(
|
||||
// rather than old display name.
|
||||
// This could be fixed by looking up the contact with the highest
|
||||
// `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 {
|
||||
better_msg = if id == from_id {
|
||||
silent = true;
|
||||
@@ -2872,70 +2882,15 @@ async fn apply_group_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
apply_chat_name_and_avatar_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
from_id,
|
||||
chat,
|
||||
&mut send_event_chat_modified,
|
||||
&mut better_msg,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if is_from_in_chat {
|
||||
if chat.member_list_is_stale(context).await? {
|
||||
@@ -2954,7 +2909,7 @@ async fn apply_group_changes(
|
||||
transaction.execute(
|
||||
"DELETE FROM chats_contacts
|
||||
WHERE chat_id=?",
|
||||
(chat_id,),
|
||||
(chat.id,),
|
||||
)?;
|
||||
|
||||
// Insert contacts with default timestamps of 0.
|
||||
@@ -2963,7 +2918,7 @@ async fn apply_group_changes(
|
||||
VALUES (?, ?)",
|
||||
)?;
|
||||
for contact_id in &new_members {
|
||||
statement.execute((chat_id, contact_id))?;
|
||||
statement.execute((chat.id, contact_id))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2975,7 +2930,7 @@ async fn apply_group_changes(
|
||||
{
|
||||
send_event_chat_modified |= update_chats_contacts_timestamps(
|
||||
context,
|
||||
chat_id,
|
||||
chat.id,
|
||||
Some(from_id),
|
||||
to_ids,
|
||||
past_ids,
|
||||
@@ -3015,7 +2970,7 @@ async fn apply_group_changes(
|
||||
chat::update_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
chat_id,
|
||||
chat.id,
|
||||
&new_members,
|
||||
)
|
||||
.await?;
|
||||
@@ -3023,7 +2978,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
|
||||
chat_id
|
||||
chat.id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
@@ -3033,7 +2988,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
||||
chat::get_chat_contacts(context, chat_id)
|
||||
chat::get_chat_contacts(context, chat.id)
|
||||
.await?
|
||||
.iter()
|
||||
.copied(),
|
||||
@@ -3063,43 +3018,12 @@ async fn apply_group_changes(
|
||||
let group_changes_msgs = if self_added {
|
||||
Vec::new()
|
||||
} 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 !new_chat_contacts.contains(&ContactId::SELF) {
|
||||
warn!(
|
||||
context,
|
||||
"Received group avatar update for group chat {chat_id} we are not a member of."
|
||||
);
|
||||
} else if !new_chat_contacts.contains(&from_id) {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
|
||||
);
|
||||
} else {
|
||||
info!(context, "Group-avatar change for {chat_id}.");
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
{
|
||||
match avatar_action {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
chat.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
}
|
||||
};
|
||||
chat.update_param(context).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
context.emit_event(EventType::ChatModified(chat.id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||
}
|
||||
Ok(GroupChangesInfo {
|
||||
better_msg,
|
||||
@@ -3113,6 +3037,109 @@ async fn apply_group_changes(
|
||||
})
|
||||
}
|
||||
|
||||
/// 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
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
{
|
||||
match avatar_action {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
chat.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
}
|
||||
};
|
||||
chat.update_param(context).await?;
|
||||
*send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
|
||||
async fn group_changes_msgs(
|
||||
context: &Context,
|
||||
@@ -3165,7 +3192,7 @@ fn mailinglist_header_listid(list_id_header: &str) -> Result<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
|
||||
/// 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
|
||||
/// 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,
|
||||
allow_creation: bool,
|
||||
list_id_header: &str,
|
||||
from_id: ContactId,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let listid = mailinglist_header_listid(list_id_header)?;
|
||||
@@ -3186,7 +3214,19 @@ async fn create_or_lookup_mailinglist(
|
||||
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 {
|
||||
// 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(
|
||||
context,
|
||||
Chattype::Mailinglist,
|
||||
chattype,
|
||||
&listid,
|
||||
&name,
|
||||
name,
|
||||
blocked,
|
||||
ProtectionStatus::Unprotected,
|
||||
param,
|
||||
@@ -3227,6 +3267,15 @@ async fn create_or_lookup_mailinglist(
|
||||
&[ContactId::SELF],
|
||||
)
|
||||
.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)))
|
||||
} else {
|
||||
info!(context, "Creating list forbidden by caller.");
|
||||
@@ -3372,6 +3421,39 @@ async fn apply_mailinglist_changes(
|
||||
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.
|
||||
async fn create_adhoc_group(
|
||||
context: &Context,
|
||||
|
||||
@@ -1711,7 +1711,7 @@ fn migrate_key_contacts(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Broadcast list
|
||||
// Broadcast channel
|
||||
160 => old_members
|
||||
.iter()
|
||||
.map(|(original, _)| {
|
||||
|
||||
@@ -237,9 +237,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Messages"))]
|
||||
Messages = 114,
|
||||
|
||||
#[strum(props(fallback = "Broadcast List"))]
|
||||
BroadcastList = 115,
|
||||
|
||||
#[strum(props(fallback = "%1$s of %2$s used"))]
|
||||
PartOfTotallUsed = 116,
|
||||
|
||||
@@ -1221,12 +1218,6 @@ pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &st
|
||||
.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.`.
|
||||
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
|
||||
translated(context, StockMessage::InvalidUnencryptedMail)
|
||||
|
||||
@@ -103,7 +103,8 @@ impl Summary {
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||
}
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
|
||||
@@ -45,6 +45,15 @@ use crate::tools::time;
|
||||
#[allow(non_upper_case_globals)]
|
||||
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.
|
||||
static CONTEXT_NAMES: LazyLock<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||
LazyLock::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
||||
@@ -1147,7 +1156,7 @@ impl Drop for InnerLogSink {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SentMessage<'a> {
|
||||
pub payload: String,
|
||||
recipients: String,
|
||||
pub recipients: String,
|
||||
pub sender_msg_id: MsgId,
|
||||
sender_context: &'a Context,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
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,
|
||||
};
|
||||
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 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 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 internet_xdc in [true, false] {
|
||||
|
||||
Reference in New Issue
Block a user