mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 10:12:24 +00:00
core, ios: fix notifications for forwarded and channel messages (#7706)
* plan: add notification flag for forwarded messages * core, ui: fix notifications for forwarded and relay messages * improvements * only send duplicate message event in NSE * query plans --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
f5b13f608f
commit
e8c98e8231
@@ -89,6 +89,7 @@ enum NSEChatEvent: Decodable, ChatAPIResult {
|
||||
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
|
||||
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest)
|
||||
case newChatItems(user: UserRef, chatItems: [AChatItem])
|
||||
case duplicateGroupMessage(user: UserRef, member: GroupMember)
|
||||
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
|
||||
@@ -101,6 +102,7 @@ enum NSEChatEvent: Decodable, ChatAPIResult {
|
||||
case .contactConnected: "contactConnected"
|
||||
case .receivedContactRequest: "receivedContactRequest"
|
||||
case .newChatItems: "newChatItems"
|
||||
case .duplicateGroupMessage: "duplicateGroupMessage"
|
||||
case .rcvFileSndCancelled: "rcvFileSndCancelled"
|
||||
case .sndFileComplete: "sndFileComplete"
|
||||
case .sndFileRcvCancelled: "sndFileRcvCancelled"
|
||||
@@ -117,6 +119,7 @@ enum NSEChatEvent: Decodable, ChatAPIResult {
|
||||
case let .newChatItems(u, chatItems):
|
||||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
case let .duplicateGroupMessage(u, member): return withUser(u, String(describing: member))
|
||||
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
|
||||
@@ -73,6 +73,16 @@ public enum NSENotificationData {
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
func replaces(_ bestAttempt: NSENotificationData) -> Bool {
|
||||
switch (self, bestAttempt) {
|
||||
case (.noNtf, .connectionEvent): true
|
||||
case (.noNtf, _): false
|
||||
case (.callInvitation, _): true
|
||||
case (_, .callInvitation): false
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the last thread in the process completes processing chat controller is suspended, and the database is closed, to avoid
|
||||
@@ -573,9 +583,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// A better logic could be to check whether we have a call in the best attempt while processing .msgInfo marker above.
|
||||
// If the best attempt is a call, and its marker is received, and the call is recent (e.g., the last 30 seconds), it would deliver at once,
|
||||
// instead of requesting further messages.
|
||||
if ntfEntity.msgBestAttemptNtf.callInvitation == nil || ntf.callInvitation != nil {
|
||||
if let bestAttempt = notificationEntities[id]?.msgBestAttemptNtf, ntf.replaces(bestAttempt) {
|
||||
notificationEntities[id]?.msgBestAttemptNtf = ntf
|
||||
} // otherwise keep call as best attempt
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
// We should not get to this branch, as notifications are not delivered for muted users.
|
||||
@@ -1022,8 +1032,8 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
let ntf: NSENotificationData = (cInfo.ntfsEnabled(chatItem: cItem) && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf
|
||||
let chatIdOrMemberId = if case let .groupRcv(groupMember) = chatItem.chatItem.chatDir {
|
||||
groupMember.id
|
||||
let chatIdOrMemberId = if case let .group(groupInfo, _) = cInfo, let memberId = cItem.meta.forwardedByMember ?? cItem.chatDir.groupMemberId {
|
||||
groupMemberEntityId(groupInfo.groupId, memberId)
|
||||
} else {
|
||||
chatItem.chatInfo.id
|
||||
}
|
||||
@@ -1031,6 +1041,8 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .duplicateGroupMessage(_, member):
|
||||
return (member.id, .noNtf)
|
||||
case let .rcvFileSndCancelled(_, aChatItem, _):
|
||||
cleanupFile(aChatItem)
|
||||
return nil
|
||||
|
||||
@@ -3255,7 +3255,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
public var relayLink: String?
|
||||
public var memberVerifiedCode: SecurityCode?
|
||||
|
||||
public var id: String { "#\(groupId) @\(groupMemberId)" }
|
||||
public var id: String { groupMemberEntityId(groupId, groupMemberId) }
|
||||
public var ready: Bool { get { activeConn?.connStatus == .ready } }
|
||||
public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } }
|
||||
public var sendMsgEnabled: Bool { get {
|
||||
@@ -3444,6 +3444,10 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
)
|
||||
}
|
||||
|
||||
public func groupMemberEntityId(_ groupId: Int64, _ groupMemberId: Int64) -> String {
|
||||
"#\(groupId) @\(groupMemberId)"
|
||||
}
|
||||
|
||||
public struct GroupSupportChat: Codable, Hashable {
|
||||
public var chatTs: Date
|
||||
public var unread: Int
|
||||
@@ -4217,6 +4221,10 @@ public enum CIDirection: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var groupMemberId: Int64? {
|
||||
if case let .groupRcv(groupMember) = self { groupMember.groupMemberId } else { nil }
|
||||
}
|
||||
|
||||
public func sameDirection(_ dir: CIDirection) -> Bool {
|
||||
switch (self, dir) {
|
||||
case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId
|
||||
@@ -4242,6 +4250,7 @@ public struct CIMeta: Decodable, Hashable {
|
||||
public var userMention: Bool
|
||||
public var deletable: Bool
|
||||
public var editable: Bool
|
||||
public var forwardedByMember: Int64?
|
||||
public var showGroupAsSender: Bool
|
||||
public var msgVerified: MsgVerified?
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ undocumentedEvents =
|
||||
"CEvtContactRequestRejected",
|
||||
"CEvtContactSwitch",
|
||||
"CEvtCustomChatEvent",
|
||||
"CEvtDuplicateGroupMessage",
|
||||
"CEvtGroupMemberRatchetSync",
|
||||
"CEvtGroupMemberSwitch",
|
||||
"CEvtServiceSubStatus",
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# Notification flag of forwarded messages
|
||||
|
||||
Date: 2026-10-13.
|
||||
|
||||
A message forwarded by a chat relay or by a forwarding group host is sent with the SMP notification flag when its original event has the flag by the rule of `hasNotification`.
|
||||
|
||||
References are to `/code/simplex-chat-4`, branch `master`, commit `2f4a443dd`. Haskell paths without a directory are in `src/Simplex/Chat/`. `SMQ/` is `dist-newstyle/src/simplexmq-f058c38b0eecb6c0/src/Simplex/Messaging/`. `NSE` is `apps/ios/SimpleX NSE/NotificationService.swift`.
|
||||
|
||||
## 1. Present behaviour
|
||||
Path of a forwarded message:
|
||||
```
|
||||
received Library/Subscriber.hs:1014 MSG of a member connection; the flag of the received SMP message is ignored
|
||||
Library/Subscriber.hs:1069-1144 processEvent: a NewMessageDeliveryTask is returned for each event to forward
|
||||
task Library/Subscriber.hs:1022 created when the user is the relay of a channel, or an admin or owner of a group (:3800), and the author is not blocked by an admin
|
||||
Library/Subscriber.hs:1158-1176 createDeliveryTasks; createMsgDeliveryTask (Store/Delivery.hs:78) is called at :1165 and :1172 only
|
||||
Store/Delivery.hs:137-170 the task is read with its message: msg_body is decoded to verifiedMsg, the original event
|
||||
job Library/Subscriber.hs:4233-4240 DJDeliveryJob: accepted tasks of one scope (Store/Delivery.hs:178) are batched into one job (Messages/Batch.hs:84)
|
||||
Library/Subscriber.hs:4246-4251 DJRelayRemoved: one job for one task
|
||||
Library/Commands.hs:3256-3265 leaveChannelRelay: one job with the x.grp.leave of the relay
|
||||
Store/Delivery.hs:285-328 the job is read as scope, sender ids, body and cursor; its events are stored only as the encoded body
|
||||
sent Library/Subscriber.hs:4379-4423 channel: for each recipient, overflow profile batches, then the body; MsgFlags False (:4416)
|
||||
Library/Subscriber.hs:4424-4487 channel support scope and regular group: deliver; MsgFlags False (:4483)
|
||||
```
|
||||
Types (`Delivery.hs`):
|
||||
```
|
||||
DeliveryTaskContext :48
|
||||
jobScope :: DeliveryJobScope
|
||||
sentAsGroup :: ShowGroupAsSender
|
||||
DeliveryJobScope :54
|
||||
DJSGroup {jobSpec :: DeliveryJobSpec}
|
||||
DJSMemberSupport {supportGMId :: GroupMemberId}
|
||||
DeliveryJobSpec :60
|
||||
DJDeliveryJob {includePending :: Bool}
|
||||
DJRelayRemoved
|
||||
NewMessageDeliveryTask :122
|
||||
messageId :: MessageId
|
||||
taskContext :: DeliveryTaskContext
|
||||
MessageDeliveryTask :128
|
||||
taskId :: Int64
|
||||
jobScope :: DeliveryJobScope
|
||||
senderGMId :: GroupMemberId
|
||||
fwdSender :: FwdSender
|
||||
brokerTs :: UTCTime
|
||||
verifiedMsg :: VerifiedMsg 'Json
|
||||
MessageDeliveryJob :161
|
||||
jobId :: Int64
|
||||
jobScope :: DeliveryJobScope
|
||||
senderGMIds :: [GroupMemberId]
|
||||
body :: ByteString
|
||||
cursorGMId_ :: Maybe GroupMemberId
|
||||
```
|
||||
Tables (`Store/SQLite/Migrations/M20250813_delivery_tasks.hs:54, :103`, `M20260529_delivery_job_senders.hs:17`; the same columns in Postgres):
|
||||
```
|
||||
both tables
|
||||
group_id
|
||||
worker_scope
|
||||
job_scope_spec_tag
|
||||
job_scope_include_pending
|
||||
job_scope_support_gm_id
|
||||
failed
|
||||
created_at
|
||||
updated_at
|
||||
delivery_tasks
|
||||
delivery_task_id
|
||||
sender_group_member_id
|
||||
message_id -- messages.chat_msg_event: the tag; messages.msg_body: the original event
|
||||
message_from_channel
|
||||
task_status
|
||||
task_err_reason
|
||||
delivery_jobs
|
||||
delivery_job_id
|
||||
sender_group_member_ids
|
||||
body
|
||||
cursor_group_member_id
|
||||
job_status
|
||||
job_err_reason
|
||||
```
|
||||
The flag, from the forwarder to the notification:
|
||||
```
|
||||
forwarder agent SMQ/Agent.hs:2186-2203, 2563 one flag for each MsgReq, stored with the sent message; a body shared by VRRef is sent with the flag of each request
|
||||
SMQ/Agent.hs:2637-2644 the flag is a parameter of SEND
|
||||
SMP server SMQ/Server.hs:2038-2039 a notification is stored for a flagged message to a queue with notifier credentials (:2131-2136), and for such a message only
|
||||
SMQ/Server.hs:361-414 the stored notification is sent as NMSG to the subscriber of the notifier, the notification server
|
||||
notification server SMQ/Notifications/Server.hs:545-553 NMSG: a push notification with the last notifications of the device token is sent to the push provider (:693-702)
|
||||
recipient Library/Subscriber.hs:890 the notification subscription of a member connection is created unless the chat is muted, MFNone (Library/Commands.hs:1922-1929; SMQ/Agent.hs:3345-3361)
|
||||
SMQ/Agent.hs:2093-2144, 3741 the agent gets the message of the notified queue and passes MSG with the flag; forwarded content is opaque to the agent
|
||||
Library/Subscriber.hs:1014 the core ignores the flag; the item is created from the forwarded event (:4008-4064)
|
||||
```
|
||||
iOS notification extension, for a forwarded message:
|
||||
```
|
||||
entity key NSE:489-498 id of the notified connection: "#<groupId> @<groupMemberId>" of the relay or the host (apps/ios/SimpleXChat/ChatTypes.swift:3624-3632, :3258)
|
||||
item key NSE:1025-1029 the member of the item, its author, for groupRcv; "#<groupId>" for channelRcv
|
||||
forwarded item NSE:173-187 the keys are different: the notification of the item is added to droppedNotifications, outside the entity of the notified connection
|
||||
shown NSE:1196-1216 the default of the connection: MFAll: title "#<group> <relay>:", text "message received" (apps/ios/SimpleXChat/Notifications.swift:102); MFMentions: empty content
|
||||
```
|
||||
- With the flag alone the push notification is sent, and the message is received by the core of the NSE: the item is created. The shown notification is the default one. The NSE change of section 7 is required for a notification with the message text.
|
||||
|
||||
Regular groups, the same message sent to a member by its author and by the host:
|
||||
```
|
||||
author Library/Internal.hs:2847-2865 member with a ready connection: sent to the member; otherwise forwarded by the host (MSAForwarded) or stored as pending
|
||||
host Library/Subscriber.hs:4447-4458 job recipients: members whose relation with the sender is MRIntroduced
|
||||
both sent while the author is connected to the member and the relation at the host is MRIntroduced
|
||||
the second copy is a duplicate: one item; the recipient sends x.grp.mem.con (Library/Internal.hs:2939-2945, 2958-2968), the host stops forwarding (Library/Subscriber.hs:3724-3729)
|
||||
rule each copy of x.msg.new is flagged: two notifications for one item in that interval, one otherwise
|
||||
```
|
||||
|
||||
## 2. Rule
|
||||
```
|
||||
forwarded message flag = hasNotification of the tag of its original event (Protocol.hs:1338)
|
||||
the flag of the received SMP message is unused
|
||||
SMP message notification = True when the flag of any forwarded message of its body is True, as for sent events (Library/Internal.hs:2248, 2748)
|
||||
job one value for the body, computed when the job is created; the same for every recipient, scope and cursor page
|
||||
channel pieces body (id 0) and extBody, profiles with the body (id 1): the job value
|
||||
overflow profile batches (id 2 and above): False; their elements are x.grp.mem.new of the relay (Library/Subscriber.hs:4277)
|
||||
```
|
||||
Events (handlers at `Library/Subscriber.hs:1080-1132`; forwarded events are those of `isForwardedGroupMsg`, `Protocol.hs:528`):
|
||||
```
|
||||
event flag
|
||||
x.msg.new True every content message, with or without a file
|
||||
x.grp.del True forwarded by the relay-removed job (:1118)
|
||||
x.msg.file.descr False
|
||||
x.msg.update False
|
||||
x.msg.del False
|
||||
x.msg.react False
|
||||
x.file.cancel False
|
||||
x.info False
|
||||
x.grp.relay.new False
|
||||
x.grp.mem.new False
|
||||
x.grp.mem.role False
|
||||
x.grp.mem.restrict False
|
||||
x.grp.mem.del False also the relay-removed job of a removed relay or host (:3746)
|
||||
x.grp.leave False also the job of a relay that leaves (Library/Commands.hs:3263)
|
||||
x.grp.info False
|
||||
x.grp.prefs False
|
||||
x.grp.roster False
|
||||
x.file True handled without a delivery task (:1096)
|
||||
x.grp.mem.fwd True handled without a delivery task (:1110)
|
||||
x.grp.inv True handled without a delivery task (:1124)
|
||||
x.call.inv True handled without a delivery task (:1132)
|
||||
x.contact True handled without a delivery task (:1132)
|
||||
```
|
||||
|
||||
## 3. Core changes
|
||||
```
|
||||
Protocol.hs
|
||||
msgHasNotification new, next to hasNotification (:1338): VerifiedMsg e -> Bool
|
||||
hasNotification of the tag of the event of the message (verifiedChatMsg, :1583)
|
||||
Delivery.hs
|
||||
MessageDeliveryJob (:161) notification :: Bool -- new, after body
|
||||
Store/Delivery.hs
|
||||
createMsgDeliveryJob (:251) new parameter notification :: Bool, before body; written to delivery_jobs.notification
|
||||
MessageDeliveryJobRow (:283) BoolInt added after the body
|
||||
getNextDeliveryJob (:309-328) notification selected after body; set in MessageDeliveryJob
|
||||
Library/Subscriber.hs
|
||||
processDeliveryTask (:4235) notification: True when msgHasNotification of verifiedMsg is True for any of acceptedTasks; computed next to senderGMIds, passed at :4237
|
||||
(:4250) msgHasNotification verifiedMsg of the task is passed
|
||||
processDeliveryJob (:4331) notification is bound from the job
|
||||
recipientBodyPieces (:4417) each piece is returned with its flag: overflow batch False; extBody and body: notification
|
||||
addPiece (:4410-4416) MsgFlags {notification = the flag of the piece}
|
||||
deliver (:4483) MsgFlags {notification}
|
||||
Library/Commands.hs
|
||||
leaveChannelRelay (:3263) hasNotification XGrpLeave_ is passed
|
||||
Library/Internal.hs
|
||||
sendFwdMemberMessage (:2903) MsgFlags {notification = msgHasNotification verifiedMsg}
|
||||
False for both callers: x.grp.roster (:1307), x.grp.mem.del (Library/Subscriber.hs:3798)
|
||||
```
|
||||
Sites with a fixed flag, unchanged:
|
||||
```
|
||||
Library/Internal.hs:1428 sendHistory: history for a new member, with the welcome message of a group; MsgFlags False (open question 1)
|
||||
Library/Internal.hs:2459 sendGroupMemberMessages: events of the user to one member; notification = True
|
||||
Library/Internal.hs:2910 sendPendingGroupMessages: pending messages of the user to one member; notification = True
|
||||
tests/ChatTests/Groups.hs:5681, 10907, 13320, 13452, 13608, 13701 elements sent by tests through the agent; MsgFlags False
|
||||
```
|
||||
- Every other `MsgFlags` value of the repository is built for events of the user, from `hasNotification` of their tags: `Library/Internal.hs:2248, 2552, 2748`; `Library/Commands.hs:2672, 4131`; `Library/Subscriber.hs:356`.
|
||||
- Forwarded content is sent as `>` elements only (`encodeFwdElement`, `Messages/Batch.hs:125`; used at `:98`, `Library/Subscriber.hs:4248`, `Library/Internal.hs:1423, 2902`). `XGrpMsgForward` events are only received (`Library/Subscriber.hs:1127`).
|
||||
|
||||
## 4. Storage
|
||||
Migration `M20261012_delivery_job_notification`, SQLite and Postgres, registered in `Store/SQLite/Migrations.hs` (`:180`, `:358`), `Store/Postgres/Migrations.hs` (`:57`, `:112`) and `simplex-chat.cabal` (`:172`, `:351`). It is one `ADD COLUMN`; indexes are unchanged.
|
||||
```
|
||||
delivery_jobs
|
||||
notification INTEGER NOT NULL DEFAULT 0 -- SMALLINT in Postgres
|
||||
```
|
||||
- Down migration: the column is dropped. `delivery_tasks` is unchanged: the event of a task is read with its message.
|
||||
- Applied migrations must be a prefix of the list sorted by name (`SMQ/Agent/Store/Migrations.hs:23-34`; `Store/SQLite/Migrations.hs:363`). The name is ordered after `20261010_invite_member_via_group` and before the unreleased `20261013_chat_layout` and `20261013_msg_priority` of branch `ep/llm-ux` (open question 3).
|
||||
- The schema dumps and `chat_query_plans.txt` are regenerated by the tests "verify and overwrite schema dump" (`tests/SchemaDump.hs:58`, `tests/PostgresSchemaDump.hs:29`) and "verify and overwrite query plans" (`tests/SchemaDump.hs:165`); they are never edited by hand.
|
||||
|
||||
## 5. Compatibility and cost
|
||||
```
|
||||
chat protocol unchanged; the flag is a parameter of SMP SEND (SMQ/Protocol.hs:598)
|
||||
recipient every version; the flag is used by the SMP server only
|
||||
earlier relay or host sends without the flag, as now; a channel member is notified by each relay of this release
|
||||
job of an earlier release notification = 0: sent without the flag
|
||||
downgrade the column is dropped
|
||||
traffic count and size of SMP messages unchanged
|
||||
notifications one for each flagged SMP message to a queue with notifier credentials: a device with a notification token and the chat set to MFAll or MFMentions
|
||||
one job is one flagged SMP message for a recipient, for all its events
|
||||
channel a post is sent by each relay to each member (Library/Internal.hs:2843-2845; Store/Delivery.hs:352)
|
||||
with N members and R relays up to N × R notifications are sent for a post; one item is created (Library/Internal.hs:2958-2959)
|
||||
shown on iOS: one notification for a post; a duplicate copy is shown with empty content (section 7)
|
||||
profile overflow batches unflagged
|
||||
```
|
||||
|
||||
## 6. Tests
|
||||
`tests/ChatTests/Groups.hs`, helpers next to `relayConnIdToMember` (`:13140`):
|
||||
```
|
||||
agentSendBody cc connId body body sent to the connection by the agent of cc, MsgFlags False; a failure is a test failure
|
||||
lastSndMsgFlags cc connId n msg_flags, "T" or "F", of the last n sent rows of type A_MSG ("M") of agent table messages for the connection, in sending order; withCCAgentTransaction (tests/ChatTests/Utils.hs:675)
|
||||
lastJobNotification cc notification of the last row of delivery_jobs; withCCTransaction
|
||||
```
|
||||
- The agent row of a sent message is kept after delivery until its receipt (`SMQ/Agent/Store/AgentStore.hs:1318-1349`; `SMQ/Agent.hs:2866-2869`): recipients run `/set receipts all off` before the checked messages (as `:1561`).
|
||||
|
||||
New tests:
|
||||
```
|
||||
group message forwarding (:191)
|
||||
testGroupMsgForwardNotification createGroup3, setupGroupForwarding (:5802); bob sends a message: alice to cath "T", job True; bob edits it: "F", job False
|
||||
relay delivery, single relay (:283)
|
||||
testChannels1RelayDeliverNotification createChannel1Relay (:9523); alice posts: bob to cath, dan and eve "T", job True; cath adds a reaction: bob to dan and eve "F", job False
|
||||
deliver member profiles via relay (:295)
|
||||
testChannelOverflowProfileNotification the scenario of testChannelLargeProfileFits (:9956) with an image of 10000 characters and a message of 7000 characters, so that the profile is sent in an overflow batch: bob to dan, the rows added by the message: "F" for the overflow profile batch, "T" for the message
|
||||
```
|
||||
- One sender and one message give one job.
|
||||
Assertions added to present tests, on `delivery_jobs` only (the connections and their agent rows are deleted after the job, `Library/Subscriber.hs:4328`):
|
||||
```
|
||||
testGroupMsgForwardGroupDeletion (:6135) alice: job True, x.grp.del
|
||||
testChannelDeleteGroupSigned (:11031) bob: job True, x.grp.del
|
||||
testGroupMsgForwardAdminRemoval (:6096) alice: job False, x.grp.mem.del
|
||||
testChannelRelayLeave (:11220) bob: job False, x.grp.leave
|
||||
```
|
||||
Present tests of delivery jobs, unchanged: "group message forwarding" (`:191-206`), "group scoped messages" (`:255-264`), "relay delivery" (`:282-292`), "deliver member profiles via relay" (`:295-301`), `testRelayBatchAllLarge` and `testRelayBatchSingletonOverflow` (`tests/MessageBatching.hs:164, 186`).
|
||||
|
||||
## 7. iOS notification of a forwarded message and of a duplicate copy
|
||||
Event, `Controller.hs:1048`, next to `CEvtUnknownMemberAnnounced`:
|
||||
```
|
||||
CEvtDuplicateGroupMessage JSON type duplicateGroupMessage
|
||||
user :: User
|
||||
member :: GroupMember -- the member whose connection delivered the copy
|
||||
```
|
||||
Emission, `Library/Internal.hs`, before the present handling of the duplicate, only when the chat is started with `main=off` (the NSE and the share extension):
|
||||
```
|
||||
isMainApp :2976 subscriptionMode is SMSubscribe; startChatController sets SMOnlyCreate for main=off (Library/Commands.hs:235)
|
||||
saveGroupRcvMsg :2925 direct copy; member: the member of the connection; every SEDuplicateGroupMessage
|
||||
x.grp.mem.con to the forwarding member when the stored copy was forwarded, as now (:2940-2944)
|
||||
saveGroupFwdRcvMsg :2959 channel; member: the forwarding relay; Nothing is returned, as now
|
||||
:2960-2969 group; member: the forwarding host; x.grp.mem.con and the error, as now
|
||||
```
|
||||
Consumers:
|
||||
```
|
||||
terminal view View.hs:471: no output, as CEvtNtfMessage (:547)
|
||||
API docs bots/src/API/Docs/Events.hs:205: in undocumentedEvents, as CEvtUnknownMemberCreated
|
||||
apps, CLI, bots not emitted: started with main=on
|
||||
NSE decoded: case duplicateGroupMessage (SimpleX NSE/NSEAPITypes.swift:91)
|
||||
```
|
||||
Swift types, `apps/ios/SimpleXChat/ChatTypes.swift`:
|
||||
```
|
||||
CIMeta.forwardedByMember :4253 Int64?, after editable, as in core CIMeta (Messages.hs:525); omitted in JSON for Nothing
|
||||
groupMemberEntityId :3447 (groupId, groupMemberId) -> "#<groupId> @<groupMemberId>"; used by GroupMember.id (:3258) and the NSE
|
||||
CIDirection.groupMemberId :4224 groupMemberId of the member of groupRcv; nil for other directions
|
||||
```
|
||||
|
||||
NSE, `apps/ios/SimpleX NSE/NotificationService.swift`:
|
||||
```
|
||||
receivedMsgNtf (:1035-1039)
|
||||
newChatItems key of a group item: groupMemberEntityId of the group and of the member whose connection delivered the item:
|
||||
forwardedByMember, otherwise chatDir.groupMemberId; the id of the connection entity (ChatTypes.swift:3624-3632)
|
||||
other items, and channelRcv without forwardedByMember: chatInfo.id, as now
|
||||
duplicateGroupMessage key: member.id; notification: .noNtf
|
||||
processReceivedNtf (:586-588): the best attempt of the entity is read from notificationEntities[id] and replaced when ntf.replaces(bestAttempt)
|
||||
NSENotificationData.replaces (:77-85), new notification, best attempt:
|
||||
.noNtf, .connectionEvent true: the default of the connection
|
||||
.noNtf, other false
|
||||
.callInvitation, any true
|
||||
other, .callInvitation false
|
||||
other, other true
|
||||
```
|
||||
- `.noNtf` is used: `.connectionEvent` is created only as the default (`defaultBestAttemptNtf`, `:1208-1229`). A received item, a contact event and a call are never replaced by `.noNtf`.
|
||||
- The best attempt is read from `notificationEntities[id]`: `processDroppedNotifications` (`:514-529`) passes the entity copied before its notifications are processed.
|
||||
|
||||
Results:
|
||||
```
|
||||
forwarded post, MFAll the text of the post (createMessageReceivedNtf, apps/ios/SimpleXChat/Notifications.swift:66)
|
||||
forwarded post, MFMentions the text for a mention or a reply; empty content otherwise
|
||||
duplicate copy empty content, unless an item of the same push notification is shown
|
||||
mention, then other item the mention is shown; before this patch, .noNtf of the other item replaced it
|
||||
```
|
||||
Tests, `tests/ChatTests/Groups.hs`; helpers next to `lastJobNotification`:
|
||||
```
|
||||
duplicateMessageEventsCfg testCfg with an eventHook (Controller.hs:257) that records the member names of CEvtDuplicateGroupMessage
|
||||
setMainAppOff cc subscriptionMode of cc set to SMOnlyCreate, as by /_start main=off; called after the group is created
|
||||
```
|
||||
Copies are sent through the agent of the sender by `agentSendBody`, as in `testGroupMsgForwardOwnMembershipRejected` (`:5666`), so that the order of copies is fixed:
|
||||
```
|
||||
group message forwarding (:191)
|
||||
testGroupMsgForwardDuplicateEvent createGroup3; bob's message is received by cath directly; alice's agent sends a forwarded copy
|
||||
(Library/Internal.hs:2960-2969): cath "duplicate group message, group id: 1"; no event
|
||||
setMainAppOff cath; alice's agent sends the copy again: the same line; event member alice
|
||||
testGroupMsgDirectDuplicateEvent alice's agent sends a forwarded copy; setMainAppOff cath; bob's agent sends the direct copy
|
||||
(Library/Internal.hs:2939-2946): cath "duplicate group message, group id: 1"; event member bob
|
||||
relay delivery, single relay (:283)
|
||||
testChannelDuplicateEvent createChannel1Relay; alice posts; setMainAppOff cath; bob's agent sends a second forwarded copy
|
||||
(Library/Internal.hs:2959): event member bob; cath without output
|
||||
```
|
||||
- The NSE and the Swift types are not compiled on this machine. Manual checks on iOS: a post in a channel with 2 relays, with MFAll and with MFMentions; a forwarded message in a group; a mention followed by an ordinary message in one push notification.
|
||||
|
||||
## Interfaces
|
||||
```
|
||||
core functions msgHasNotification :: VerifiedMsg e -> Bool
|
||||
createMsgDeliveryJob, new parameter notification
|
||||
isMainApp :: CM Bool
|
||||
core types MessageDeliveryJob.notification
|
||||
core events CEvtDuplicateGroupMessage, JSON type duplicateGroupMessage
|
||||
storage delivery_jobs.notification
|
||||
migration M20261012_delivery_job_notification
|
||||
iOS CIMeta.forwardedByMember
|
||||
groupMemberEntityId
|
||||
CIDirection.groupMemberId
|
||||
NSEChatEvent.duplicateGroupMessage
|
||||
NSENotificationData.replaces
|
||||
test helpers agentSendBody
|
||||
lastSndMsgFlags
|
||||
lastJobNotification
|
||||
duplicateMessageEventsCfg
|
||||
setMainAppOff
|
||||
```
|
||||
- `eventHasNotification :: ChatMsgEvent e -> Bool` of `2026-10-13-message-priority.md` is accepted by one change: `msgHasNotification` is computed as `eventHasNotification` of the event of the message. Its three uses, the job column and `hasNotification XGrpLeave_` in `leaveChannelRelay` are unchanged.
|
||||
- With that change a forwarded `x.msg.update` that raises priority is flagged. The line of the priority plan, section 4, "delivery jobs of relays and forwarding hosts, MsgFlags False for every message" is replaced by this plan.
|
||||
|
||||
## Open questions
|
||||
1. History and the welcome message (`sendHistory`), flagged until commit `93831a0f2` (2026-07-17) and unflagged since: apply the rule? Recommended: unchanged, sent without the flag.
|
||||
2. The job flag as a column, or computed by the job worker from the decoded body, without a migration? Recommended: the column, computed where the events of the tasks are read.
|
||||
3. Release order with `ep/llm-ux`. Recommended: this correction first, with date `20261012` in the migration name; otherwise a date after the last released migration.
|
||||
@@ -170,6 +170,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260925_badge_bindings
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20261012_delivery_job_notification
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -349,6 +350,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260925_badge_bindings
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20261012_delivery_job_notification
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
@@ -1046,6 +1046,7 @@ data ChatEvent
|
||||
| CEvtUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember}
|
||||
| CEvtUnknownMemberBlocked {user :: User, groupInfo :: GroupInfo, blockedByMember :: GroupMember, member :: GroupMember}
|
||||
| CEvtUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember}
|
||||
| CEvtDuplicateGroupMessage {user :: User, member :: GroupMember}
|
||||
| CEvtGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, msgSigned :: Maybe MsgSigStatus}
|
||||
| CEvtGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember, msgSigned :: Maybe MsgSigStatus} -- there is the same command response
|
||||
| CEvtAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
|
||||
@@ -163,6 +163,7 @@ data MessageDeliveryJob = MessageDeliveryJob
|
||||
jobScope :: DeliveryJobScope,
|
||||
senderGMIds :: [GroupMemberId],
|
||||
body :: ByteString,
|
||||
notification :: Bool,
|
||||
cursorGMId_ :: Maybe GroupMemberId
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -3260,7 +3260,7 @@ processChatCommand cxt nm = \case
|
||||
withFastStore' $ \db -> do
|
||||
deleteGroupDeliveryTasks db gInfo
|
||||
deleteGroupDeliveryJobs db gInfo
|
||||
createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) [] body
|
||||
createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) [] (hasNotification XGrpLeave_) body
|
||||
lift . void $ getDeliveryJobWorker True (groupId, DWSGroup)
|
||||
pure msg
|
||||
leaveGroupSendMsg user g@(GIK gInfo _) = do
|
||||
|
||||
@@ -2900,7 +2900,7 @@ sendFwdMemberMessage :: GroupMember -> GrpMsgForward -> VerifiedMsg 'Json -> CM
|
||||
sendFwdMemberMessage member fwd verifiedMsg =
|
||||
forM_ (readyMemberConn member) $ \(_, conn) -> do
|
||||
let body = encodeBinaryBatch [encodeFwdElement fwd verifiedMsg]
|
||||
void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags False, VRValue Nothing body)]
|
||||
void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags {notification = msgHasNotification verifiedMsg}, VRValue Nothing body)]
|
||||
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
sendPendingGroupMessages :: User -> GroupInfo -> GroupMember -> Connection -> CM ()
|
||||
@@ -2937,11 +2937,13 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta
|
||||
msg <-
|
||||
withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId)
|
||||
`catchAllErrors` \e -> case e of
|
||||
ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do
|
||||
cxt <- chatStoreCxt
|
||||
fm <- withStore $ \db -> getGroupMember db cxt user groupId forwardedByGroupMemberId
|
||||
forM_ (memberConn fm) $ \fmConn ->
|
||||
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemId) groupId
|
||||
ChatErrorStore (SEDuplicateGroupMessage _ _ _ forwardedByGroupMemberId_) -> do
|
||||
unlessM isMainApp $ toView $ CEvtDuplicateGroupMessage user am'
|
||||
forM_ forwardedByGroupMemberId_ $ \forwardedByGroupMemberId -> do
|
||||
cxt <- chatStoreCxt
|
||||
fm <- withStore $ \db -> getGroupMember db cxt user groupId forwardedByGroupMemberId
|
||||
forM_ (memberConn fm) $ \fmConn ->
|
||||
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemId) groupId
|
||||
throwError e
|
||||
_ -> throwError e
|
||||
pure (am', conn', msg)
|
||||
@@ -2955,20 +2957,25 @@ saveGroupFwdRcvMsg user gInfo@GroupInfo {groupId} forwardingMember refAuthorMemb
|
||||
-- TODO [relays] TBC highlighting difference between deduplicated messages (useRelays branch)
|
||||
withStore' (\db -> runExceptT $ createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) >>= \case
|
||||
Right msg -> pure $ Just msg
|
||||
Left e@SEDuplicateGroupMessage {authorGroupMemberId, forwardedByGroupMemberId}
|
||||
| useRelays' gInfo -> pure Nothing -- with chat relays, duplicates are expected
|
||||
| otherwise -> case (authorGroupMemberId, forwardedByGroupMemberId) of
|
||||
(Just authorGMId, Nothing) -> do
|
||||
cxt <- chatStoreCxt
|
||||
am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db cxt user groupId authorGMId
|
||||
if maybe False (\ref -> sameMemberId (memberId' ref) am) refAuthorMember_
|
||||
then forM_ (memberConn forwardingMember) $ \fmConn ->
|
||||
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId
|
||||
else toView $ CEvtMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id"
|
||||
throwError $ ChatErrorStore e
|
||||
_ -> throwError $ ChatErrorStore e
|
||||
Left e@SEDuplicateGroupMessage {authorGroupMemberId, forwardedByGroupMemberId} -> do
|
||||
unlessM isMainApp $ toView $ CEvtDuplicateGroupMessage user forwardingMember
|
||||
if
|
||||
| useRelays' gInfo -> pure Nothing -- with chat relays, duplicates are expected
|
||||
| otherwise -> case (authorGroupMemberId, forwardedByGroupMemberId) of
|
||||
(Just authorGMId, Nothing) -> do
|
||||
cxt <- chatStoreCxt
|
||||
am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db cxt user groupId authorGMId
|
||||
if maybe False (\ref -> sameMemberId (memberId' ref) am) refAuthorMember_
|
||||
then forM_ (memberConn forwardingMember) $ \fmConn ->
|
||||
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId
|
||||
else toView $ CEvtMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id"
|
||||
throwError $ ChatErrorStore e
|
||||
_ -> throwError $ ChatErrorStore e
|
||||
Left e -> throwError $ ChatErrorStore e
|
||||
|
||||
isMainApp :: CM Bool
|
||||
isMainApp = (SMSubscribe ==) <$> chatReadVar subscriptionMode
|
||||
|
||||
saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd)
|
||||
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False
|
||||
|
||||
|
||||
@@ -4233,8 +4233,9 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do
|
||||
withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do
|
||||
let (body_, acceptedTasks, largeTasks) = batchDeliveryTasks1 (vr cxt) maxForwardBatchLength nextTasks
|
||||
senderGMIds = S.toList . S.fromList $ map (\MessageDeliveryTask {senderGMId} -> senderGMId) acceptedTasks
|
||||
notification = any (\MessageDeliveryTask {verifiedMsg} -> msgHasNotification verifiedMsg) acceptedTasks
|
||||
withStore' $ \db -> do
|
||||
forM_ body_ $ \body -> createMsgDeliveryJob db gInfo jobScope senderGMIds body
|
||||
forM_ body_ $ \body -> createMsgDeliveryJob db gInfo jobScope senderGMIds notification body
|
||||
forM_ acceptedTasks $ \t -> updateDeliveryTaskStatus db (deliveryTaskId t) DTSProcessed
|
||||
forM_ largeTasks $ \t -> setDeliveryTaskErrStatus db (deliveryTaskId t) "large"
|
||||
when (isJust body_) . lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
@@ -4247,7 +4248,7 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do
|
||||
fwd = GrpMsgForward {fwdSender, fwdBrokerTs}
|
||||
body = encodeBinaryBatch [encodeFwdElement fwd verifiedMsg]
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope [senderGMId] body
|
||||
createMsgDeliveryJob db gInfo jobScope [senderGMId] (msgHasNotification verifiedMsg) body
|
||||
updateDeliveryTaskStatus db (deliveryTaskId task) DTSProcessed
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
|
||||
@@ -4328,7 +4329,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
deleteGroupConnections user gInfo True
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
where
|
||||
MessageDeliveryJob {jobId, jobScope, senderGMIds, body, cursorGMId_ = startingCursor} = job
|
||||
MessageDeliveryJob {jobId, jobScope, senderGMIds, body, notification, cursorGMId_ = startingCursor} = job
|
||||
singleSenderGMId_ = case senderGMIds of
|
||||
[s] -> Just s
|
||||
_ -> Nothing
|
||||
@@ -4407,16 +4408,16 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
addRecipient acc r = case readyMemberConn r of
|
||||
Just (_, conn) -> snd $ foldl' (addPiece conn) (0 :: Int, acc) (recipientBodyPieces r)
|
||||
Nothing -> acc
|
||||
addPiece conn (k, (issued, reqs)) (bid, msgBody) =
|
||||
addPiece conn (k, (issued, reqs)) (bid, ntf, msgBody) =
|
||||
let vor
|
||||
| IS.member bid issued = VRRef bid
|
||||
| otherwise = VRValue (Just bid) msgBody
|
||||
issued' = IS.insert bid issued
|
||||
connId = if k == 0 then aConnId conn else B.empty
|
||||
in (k + 1, (issued', (connId, PQEncOff, MsgFlags False, vor) : reqs))
|
||||
in (k + 1, (issued', (connId, PQEncOff, MsgFlags {notification = ntf}, vor) : reqs))
|
||||
recipientBodyPieces r =
|
||||
[(i, b) | (i, (b, ss)) <- overflowWithIds, any missing ss]
|
||||
<> [if any missing inBodySenders then (1, extBody) else (0, body)]
|
||||
[(i, False, b) | (i, (b, ss)) <- overflowWithIds, any missing ss]
|
||||
<> [if any missing inBodySenders then (1, notification, extBody) else (0, notification, body)]
|
||||
where
|
||||
missing s = case M.lookup (groupMemberId' s) senderVec of
|
||||
Just vec -> getRelation (indexInGroup r) vec == MRNew
|
||||
@@ -4480,7 +4481,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
addReq conn (memIdx_, reqs) =
|
||||
(subtract 1 <$> memIdx_, req : reqs)
|
||||
where
|
||||
req = (aConnId conn, PQEncOff, MsgFlags False, vrValue_)
|
||||
req = (aConnId conn, PQEncOff, MsgFlags {notification}, vrValue_)
|
||||
vrValue_ = case memIdx_ of
|
||||
Nothing -> VRValue Nothing msgBody -- sending to one member, do not reference body
|
||||
Just 1 -> VRValue (Just 1) msgBody
|
||||
|
||||
@@ -1346,6 +1346,11 @@ hasNotification = \case
|
||||
XCallInv_ -> True
|
||||
_ -> False
|
||||
|
||||
msgHasNotification :: VerifiedMsg e -> Bool
|
||||
msgHasNotification verifiedMsg = hasNotification $ toCMEventTag chatMsgEvent
|
||||
where
|
||||
ChatMessage {chatMsgEvent} = verifiedChatMsg verifiedMsg
|
||||
|
||||
hasDeliveryReceipt :: CMEventTag e -> Bool
|
||||
hasDeliveryReceipt = \case
|
||||
XMsgNew_ -> True
|
||||
|
||||
@@ -248,8 +248,8 @@ deleteDoneDeliveryTasks db createdAtCutoff = do
|
||||
|]
|
||||
(createdAtCutoff, DTSProcessed, DTSError)
|
||||
|
||||
createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> [GroupMemberId] -> ByteString -> IO ()
|
||||
createMsgDeliveryJob db gInfo jobScope senderGMIds body = do
|
||||
createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> [GroupMemberId] -> Bool -> ByteString -> IO ()
|
||||
createMsgDeliveryJob db gInfo jobScope senderGMIds notification body = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
@@ -257,10 +257,10 @@ createMsgDeliveryJob db gInfo jobScope senderGMIds body = do
|
||||
INSERT INTO delivery_jobs (
|
||||
group_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
sender_group_member_ids, body, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
sender_group_member_ids, body, notification, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (senderColumn, Binary body, DJSPending, currentTs, currentTs))
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (senderColumn, Binary body, BI notification, DJSPending, currentTs, currentTs))
|
||||
where
|
||||
GroupInfo {groupId} = gInfo
|
||||
-- NULL ↔ []; non-empty list ↔ comma-separated decimal Int64s.
|
||||
@@ -280,7 +280,7 @@ getPendingDeliveryJobScopes db =
|
||||
|]
|
||||
(Only DJSPending)
|
||||
|
||||
type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe Text, Binary ByteString, Maybe GroupMemberId)
|
||||
type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe Text, Binary ByteString, BoolInt, Maybe GroupMemberId)
|
||||
|
||||
getNextDeliveryJob :: DB.Connection -> DeliveryWorkerKey -> IO (Either StoreError (Maybe MessageDeliveryJob))
|
||||
getNextDeliveryJob db deliveryKey = do
|
||||
@@ -310,14 +310,14 @@ getNextDeliveryJob db deliveryKey = do
|
||||
SELECT
|
||||
delivery_job_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
sender_group_member_ids, body, cursor_group_member_id
|
||||
sender_group_member_ids, body, notification, cursor_group_member_id
|
||||
FROM delivery_jobs
|
||||
WHERE delivery_job_id = ?
|
||||
|]
|
||||
(Only jobId)
|
||||
where
|
||||
toDeliveryJob :: MessageDeliveryJobRow -> Either StoreError MessageDeliveryJob
|
||||
toDeliveryJob ((Only jobId') :. jobScopeRow :. (senderGMIdsText_, Binary body, cursorGMId_)) = do
|
||||
toDeliveryJob ((Only jobId') :. jobScopeRow :. (senderGMIdsText_, Binary body, BI notification, cursorGMId_)) = do
|
||||
jobScope <- maybe (Left $ SEInvalidDeliveryJob jobId') Right $ toJobScope_ jobScopeRow
|
||||
-- NULL or empty string means []; otherwise the value must parse
|
||||
-- as a comma-separated decimal Int64 list. An unparseable
|
||||
@@ -325,7 +325,7 @@ getNextDeliveryJob db deliveryKey = do
|
||||
senderGMIds <- case senderGMIdsText_ of
|
||||
Nothing -> Right []
|
||||
Just t -> maybe (Left $ SEInvalidDeliveryJob jobId') Right $ parseSenderGMIds t
|
||||
Right $ MessageDeliveryJob {jobId = jobId', jobScope, senderGMIds, body, cursorGMId_}
|
||||
Right $ MessageDeliveryJob {jobId = jobId', jobScope, senderGMIds, body, notification, cursorGMId_}
|
||||
parseSenderGMIds :: Text -> Maybe [GroupMemberId]
|
||||
parseSenderGMIds t
|
||||
| T.null t = Just []
|
||||
|
||||
@@ -55,6 +55,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260925_badge_bindings
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20261012_delivery_job_notification
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -109,7 +110,8 @@ schemaMigrations =
|
||||
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
|
||||
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group),
|
||||
("20261012_delivery_job_notification", m20261012_delivery_job_notification, Just down_m20261012_delivery_job_notification)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20261012_delivery_job_notification where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20261012_delivery_job_notification :: Text
|
||||
m20261012_delivery_job_notification =
|
||||
[r|
|
||||
ALTER TABLE delivery_jobs ADD COLUMN notification SMALLINT NOT NULL DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20261012_delivery_job_notification :: Text
|
||||
down_m20261012_delivery_job_notification =
|
||||
[r|
|
||||
ALTER TABLE delivery_jobs DROP COLUMN notification;
|
||||
|]
|
||||
@@ -178,6 +178,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260925_badge_bindings
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20261012_delivery_job_notification
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -355,7 +356,8 @@ schemaMigrations =
|
||||
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
|
||||
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group),
|
||||
("20261012_delivery_job_notification", m20261012_delivery_job_notification, Just down_m20261012_delivery_job_notification)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20261012_delivery_job_notification where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20261012_delivery_job_notification :: Query
|
||||
m20261012_delivery_job_notification =
|
||||
[sql|
|
||||
ALTER TABLE delivery_jobs ADD COLUMN notification INTEGER NOT NULL DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20261012_delivery_job_notification :: Query
|
||||
down_m20261012_delivery_job_notification =
|
||||
[sql|
|
||||
ALTER TABLE delivery_jobs DROP COLUMN notification;
|
||||
|]
|
||||
@@ -614,15 +614,6 @@ Query:
|
||||
Plan:
|
||||
SEARCH address_ratchet_keys USING INDEX idx_address_ratchet_keys (conn_id=? AND ratchet_key_id=?)
|
||||
|
||||
Query:
|
||||
UPDATE rcv_messages
|
||||
SET receive_attempts = receive_attempts + 1
|
||||
WHERE conn_id = ? AND internal_id = ?
|
||||
RETURNING receive_attempts
|
||||
|
||||
Plan:
|
||||
SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?)
|
||||
|
||||
Query:
|
||||
DELETE FROM address_ratchet_keys
|
||||
WHERE conn_id = ?
|
||||
@@ -1431,6 +1422,10 @@ Query: SELECT last_insert_rowid()
|
||||
Plan:
|
||||
SCAN CONSTANT ROW
|
||||
|
||||
Query: SELECT msg_flags FROM messages WHERE conn_id = ? AND internal_snd_id IS NOT NULL AND msg_type = ? ORDER BY internal_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH messages USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT ratchet_state FROM ratchets WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
@@ -774,7 +774,7 @@ Query:
|
||||
SELECT
|
||||
delivery_job_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
sender_group_member_ids, body, cursor_group_member_id
|
||||
sender_group_member_ids, body, notification, cursor_group_member_id
|
||||
FROM delivery_jobs
|
||||
WHERE delivery_job_id = ?
|
||||
|
||||
@@ -5058,8 +5058,8 @@ Query:
|
||||
INSERT INTO delivery_jobs (
|
||||
group_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
sender_group_member_ids, body, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
sender_group_member_ids, body, notification, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
|
||||
@@ -7838,6 +7838,10 @@ Query: SELECT note_folder_id FROM note_folders WHERE user_id = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?)
|
||||
|
||||
Query: SELECT notification FROM delivery_jobs ORDER BY delivery_job_id DESC LIMIT 1
|
||||
Plan:
|
||||
SCAN delivery_jobs
|
||||
|
||||
Query: SELECT purchase_key FROM badge_code_redemptions
|
||||
Plan:
|
||||
SCAN badge_code_redemptions
|
||||
|
||||
@@ -806,7 +806,8 @@ CREATE TABLE delivery_jobs(
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
,
|
||||
sender_group_member_ids TEXT
|
||||
sender_group_member_ids TEXT,
|
||||
notification INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE group_member_status_predicates(
|
||||
member_status TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
@@ -469,6 +469,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
|
||||
CEvtUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um]
|
||||
CEvtUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um]
|
||||
CEvtUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m]
|
||||
CEvtDuplicateGroupMessage {} -> []
|
||||
CEvtRcvFileDescrReady _ _ _ _ -> []
|
||||
CEvtRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
|
||||
CEvtRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||
|
||||
+204
-4
@@ -18,7 +18,7 @@ import ChatTests.Profiles (addTestBadge, futureDate, issueTestBadge, testBadgeKe
|
||||
import ChatTests.Utils
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM (atomically)
|
||||
import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVarIO, writeTVar)
|
||||
import Control.Monad (forM_, void, when)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.Reader (runReaderT)
|
||||
@@ -32,14 +32,14 @@ import Data.List (intercalate, isInfixOf, isSuffixOf)
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Badges (FileSizeLimits (..))
|
||||
import Simplex.Chat.Controller (ChatController (ChatController, smpAgent), ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks, withAgent)
|
||||
import Simplex.Chat.Controller (ChatController (ChatController, smpAgent, subscriptionMode), ChatConfig (..), ChatEvent (CEvtDuplicateGroupMessage), ChatHooks (..), ChatLogLevel (..), defaultChatHooks, withAgent)
|
||||
import Simplex.Chat.Library.Internal (encodeConnInfo, getShortLinkConnReq, uniqueMsgMentions, updatedMentionNames)
|
||||
import Simplex.Chat.Markdown (parseMaybeMarkdownList)
|
||||
import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId)
|
||||
import Simplex.Chat.Messages.Batch (encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText)
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XGrpRelayInv, XInfo, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), mcSimple, msgContentText)
|
||||
import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XGrpRelayInv, XInfo, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), chatMsgToBody, mcSimple, msgContentText)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation)
|
||||
import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..))
|
||||
@@ -47,7 +47,7 @@ import Simplex.Messaging.Agent (joinConnection, prepareConnectionToJoin, sendMes
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..))
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BBS (bbsKeyGen)
|
||||
import Simplex.Messaging.Client (pattern NRMInteractive)
|
||||
@@ -194,7 +194,10 @@ chatGroupTests = do
|
||||
it "forward batched messages" testGroupMsgForwardBatched
|
||||
it "forward reports to moderators, don't forward to members (x.msg.new, MCReport)" testGroupMsgForwardReport
|
||||
it "deduplicate forwarded messages" testGroupMsgForwardDeduplicate
|
||||
it "report duplicate when forwarded copy is received after direct copy" testGroupMsgForwardDuplicateEvent
|
||||
it "report duplicate when direct copy is received after forwarded copy" testGroupMsgDirectDuplicateEvent
|
||||
it "forward message edit (x.msg.update)" testGroupMsgForwardEdit
|
||||
it "set notification flag of forwarded messages by the original event" testGroupMsgForwardNotification
|
||||
it "forward message reaction (x.msg.react)" testGroupMsgForwardReaction
|
||||
it "forward message deletion (x.msg.del)" testGroupMsgForwardDeletion
|
||||
it "forward file (x.msg.file.descr)" testGroupMsgForwardFile
|
||||
@@ -282,6 +285,8 @@ chatGroupTests = do
|
||||
describe "relay delivery" $ do
|
||||
describe "single relay" $ do
|
||||
it "should deliver messages to members" testChannels1RelayDeliver
|
||||
it "should set notification flag of delivered messages" testChannels1RelayDeliverNotification
|
||||
it "should report duplicate message delivered by relay" testChannelDuplicateEvent
|
||||
describe "should deliver messages in a loop over members" $ do
|
||||
it "number of recipients is multiple of bucket size (3/1)" (testChannels1RelayDeliverLoop 1)
|
||||
it "number of recipients is NOT multiple of bucket size (3/2)" (testChannels1RelayDeliverLoop 2)
|
||||
@@ -298,6 +303,7 @@ chatGroupTests = do
|
||||
it "multi senders disseminate independently" testChannelMultiSendersIndependent
|
||||
it "large profile fits in body" testChannelLargeProfileFits
|
||||
it "multiple large profiles pack across batches in one multi-sender job" testChannelMultipleLargeProfiles
|
||||
it "overflow profile batch is sent without notification flag" testChannelOverflowProfileNotification
|
||||
it "profile update reuses existing announcement (no re-prepend)" testChannelProfileUpdateNoRePrepend
|
||||
describe "channel operations" $ do
|
||||
it "should update channel profile (signed)" testChannelUpdateProfileSigned
|
||||
@@ -5912,6 +5918,53 @@ testGroupMsgForwardDeduplicate =
|
||||
cath <#. "#team bob> hi there"
|
||||
cath <# "#team hey team"
|
||||
|
||||
testGroupMsgForwardDuplicateEvent :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgForwardDuplicateEvent ps = do
|
||||
(dups, cfg) <- duplicateMessageEventsCfg
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
groupForwardingRelations alice "bob" "cath" `shouldEventuallyReturn` (MRConnected, MRConnected)
|
||||
|
||||
bob #> "#team hi there"
|
||||
[alice, cath] *<# "#team bob> hi there"
|
||||
|
||||
sharedMsgId <- itemSharedMsgId cath
|
||||
bobMemId <- memberIdByName cath "bob"
|
||||
connId <- relayConnIdToMember alice "cath"
|
||||
ts <- getCurrentTime
|
||||
let chatMsg = ChatMessage chatInitialVRange (Just sharedMsgId) (XMsgNew $ mcSimple $ MCText "hi there")
|
||||
sendForwardedCopy = agentSendBody alice connId $ encodeBinaryBatch [encodeFwdElement (GrpMsgForward (FwdMember bobMemId "bob") ts) (VMUnsigned chatMsg)]
|
||||
sendForwardedCopy
|
||||
cath <##. "duplicate group message, group id: 1"
|
||||
readTVarIO dups `shouldReturn` []
|
||||
setMainAppOff cath
|
||||
sendForwardedCopy
|
||||
cath <##. "duplicate group message, group id: 1"
|
||||
readTVarIO dups `shouldReturn` ["alice"]
|
||||
|
||||
testGroupMsgDirectDuplicateEvent :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgDirectDuplicateEvent ps = do
|
||||
(dups, cfg) <- duplicateMessageEventsCfg
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
bobMemId <- memberIdByName cath "bob"
|
||||
aliceConnId <- relayConnIdToMember alice "cath"
|
||||
bobConnId <- relayConnIdToMember bob "cath"
|
||||
ts <- getCurrentTime
|
||||
let chatMsg = ChatMessage chatInitialVRange (Just $ SharedMsgId "duplicate_msg1") (XMsgNew $ mcSimple $ MCText "hi there")
|
||||
agentSendBody alice aliceConnId $ encodeBinaryBatch [encodeFwdElement (GrpMsgForward (FwdMember bobMemId "bob") ts) (VMUnsigned chatMsg)]
|
||||
cath <# "#team bob> hi there [>>]"
|
||||
|
||||
setMainAppOff cath
|
||||
agentSendBody bob bobConnId $ chatMsgToBody chatMsg
|
||||
cath <##. "duplicate group message, group id: 1"
|
||||
readTVarIO dups `shouldReturn` ["bob"]
|
||||
|
||||
testGroupMsgForwardEdit :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgForwardEdit =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -5936,6 +5989,29 @@ testGroupMsgForwardEdit =
|
||||
cath ##> "/tail #team 1"
|
||||
cath <# "#team bob> hello there [>>]"
|
||||
|
||||
testGroupMsgForwardNotification :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgForwardNotification =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
setupGroupForwarding alice bob cath
|
||||
cath ##> "/set receipts all off"
|
||||
cath <## "ok"
|
||||
cathConnId <- relayConnIdToMember alice "cath"
|
||||
|
||||
bob #> "#team hi there"
|
||||
alice <# "#team bob> hi there"
|
||||
cath <# "#team bob> hi there [>>]"
|
||||
lastSndMsgFlags alice cathConnId 1 `shouldReturn` ["T"]
|
||||
lastJobNotification alice `shouldReturn` True
|
||||
|
||||
bob ##> "! #team hello there"
|
||||
bob <# "#team [edited] hello there"
|
||||
alice <# "#team bob> [edited] hello there"
|
||||
cath <# "#team bob> [edited] hello there"
|
||||
lastSndMsgFlags alice cathConnId 1 `shouldReturn` ["F"]
|
||||
lastJobNotification alice `shouldReturn` False
|
||||
|
||||
testGroupMsgForwardReaction :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgForwardReaction =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -6118,6 +6194,7 @@ testGroupMsgForwardAdminRemoval =
|
||||
alice <## "use /d #team to delete the group",
|
||||
cath <## "#team: bob removed alice from the group (signed)"
|
||||
]
|
||||
lastJobNotification alice `shouldReturn` False
|
||||
|
||||
-- there is no forwarding admin anymore between bob and cath, so messages don't get delivered
|
||||
-- (this is not a desired behavior, just a test demonstration/proof of current implementation)
|
||||
@@ -6159,6 +6236,7 @@ testGroupMsgForwardGroupDeletion =
|
||||
cath <## "#team: bob deleted the group (signed)"
|
||||
cath <## "use /d #team to delete the local copy of the group"
|
||||
]
|
||||
lastJobNotification alice `shouldReturn` True
|
||||
|
||||
alice ##> "/groups"
|
||||
alice <## "#team (group deleted, delete local copy: /d #team)"
|
||||
@@ -9520,6 +9598,65 @@ testChannels1RelayDeliver ps =
|
||||
cath <## "group ID: 1"
|
||||
cath <## "subscribers: 4"
|
||||
|
||||
testChannels1RelayDeliverNotification :: HasCallStack => TestParams -> IO ()
|
||||
testChannels1RelayDeliverNotification ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do
|
||||
withNewTestChat ps "cath" cathProfile $ \cath -> do
|
||||
withNewTestChat ps "dan" danProfile $ \dan -> do
|
||||
withNewTestChat ps "eve" eveProfile $ \eve -> do
|
||||
createChannel1Relay "team" alice bob cath dan eve
|
||||
forM_ [cath, dan, eve] $ \cc -> do
|
||||
cc ##> "/set receipts all off"
|
||||
cc <## "ok"
|
||||
cathConnId <- relayConnIdToMember bob "cath"
|
||||
danConnId <- relayConnIdToMember bob "dan"
|
||||
eveConnId <- relayConnIdToMember bob "eve"
|
||||
|
||||
alice #> "#team hi"
|
||||
bob <# "#team> hi"
|
||||
[cath, dan, eve] *<# "#team> hi [>>]"
|
||||
forM_ [cathConnId, danConnId, eveConnId] $ \connId ->
|
||||
lastSndMsgFlags bob connId 1 `shouldReturn` ["T"]
|
||||
lastJobNotification bob `shouldReturn` True
|
||||
|
||||
cath ##> "+1 #team hi"
|
||||
cath <## "added 👍"
|
||||
bob <# "#team cath> > hi"
|
||||
bob <## " + 👍"
|
||||
alice <# "#team cath> > hi"
|
||||
alice <## " + 👍"
|
||||
dan <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
dan <# "#team cath> > hi"
|
||||
dan <## " + 👍"
|
||||
eve <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
eve <# "#team cath> > hi"
|
||||
eve <## " + 👍"
|
||||
forM_ [danConnId, eveConnId] $ \connId ->
|
||||
lastSndMsgFlags bob connId 1 `shouldReturn` ["F"]
|
||||
lastJobNotification bob `shouldReturn` False
|
||||
|
||||
testChannelDuplicateEvent :: HasCallStack => TestParams -> IO ()
|
||||
testChannelDuplicateEvent ps = do
|
||||
(dups, cfg) <- duplicateMessageEventsCfg
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "dan" danProfile $ \dan ->
|
||||
withNewTestChat ps "eve" eveProfile $ \eve -> do
|
||||
createChannel1Relay "team" alice bob cath dan eve
|
||||
alice #> "#team hi"
|
||||
bob <# "#team> hi"
|
||||
[cath, dan, eve] *<# "#team> hi [>>]"
|
||||
|
||||
sharedMsgId <- itemSharedMsgId cath
|
||||
connId <- relayConnIdToMember bob "cath"
|
||||
ts <- getCurrentTime
|
||||
let chatMsg = ChatMessage chatInitialVRange (Just sharedMsgId) (XMsgNew $ mcSimple $ MCText "hi")
|
||||
setMainAppOff cath
|
||||
agentSendBody bob connId $ encodeBinaryBatch [encodeFwdElement (GrpMsgForward FwdChannel ts) (VMUnsigned chatMsg)]
|
||||
readTVarIO dups `shouldEventuallyReturn` ["bob"]
|
||||
|
||||
createChannel1Relay :: String -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO ()
|
||||
createChannel1Relay gName owner relay cath dan eve = do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay gName owner relay
|
||||
@@ -9979,6 +10116,34 @@ testChannelLargeProfileFits ps =
|
||||
|
||||
memberIntroducedTo bob "cath" "dan"
|
||||
|
||||
testChannelOverflowProfileNotification :: HasCallStack => TestParams -> IO ()
|
||||
testChannelOverflowProfileNotification ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do
|
||||
withNewTestChat ps "cath" cathProfile $ \cath -> do
|
||||
withNewTestChat ps "dan" danProfile $ \dan -> do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
|
||||
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
|
||||
memberJoinChannel "team" [bob] [alice] shortLink fullLink dan
|
||||
promoteChannelMember "team" alice bob cath [dan]
|
||||
dan ##> "/set receipts all off"
|
||||
dan <## "ok"
|
||||
danConnId <- relayConnIdToMember bob "dan"
|
||||
|
||||
let bigImage = T.pack ("data:image/png;base64," <> replicate 10000 'A')
|
||||
longText = replicate 7000 'x'
|
||||
withCCTransaction bob $ \db ->
|
||||
DB.execute db "UPDATE contact_profiles SET image = ? WHERE display_name = ?" (bigImage, "cath" :: T.Text)
|
||||
sentBefore <- length <$> lastSndMsgFlags bob danConnId 100
|
||||
|
||||
cath #> ("#team " <> longText)
|
||||
bob <# ("#team cath> " <> longText)
|
||||
alice <# ("#team cath> " <> longText <> " [>>]")
|
||||
dan <### [EndsWith "updated to cath"]
|
||||
dan <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
dan <# ("#team cath> " <> longText <> " [>>]")
|
||||
drop sentBefore <$> lastSndMsgFlags bob danConnId 100 `shouldReturn` ["F", "T"]
|
||||
|
||||
testChannelMultipleLargeProfiles :: HasCallStack => TestParams -> IO ()
|
||||
testChannelMultipleLargeProfiles ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
@@ -11053,6 +11218,7 @@ testChannelDeleteGroupSigned ps =
|
||||
eve <## "#team: alice deleted the group (signed)"
|
||||
eve <## "use /d #team to delete the local copy of the group"
|
||||
]
|
||||
lastJobNotification bob `shouldReturn` True
|
||||
|
||||
testChannelDeleteGroupCleanup :: HasCallStack => TestParams -> IO ()
|
||||
testChannelDeleteGroupCleanup ps =
|
||||
@@ -11246,6 +11412,7 @@ testChannelRelayLeave ps =
|
||||
dan <## "#team: bob left the group (signed)",
|
||||
eve <## "#team: bob left the group (signed)"
|
||||
]
|
||||
lastJobNotification bob `shouldReturn` False
|
||||
|
||||
-- verify relay1 member status is "left" on all clients that know bob
|
||||
checkMemberStatus alice "bob" (Just "left")
|
||||
@@ -13149,6 +13316,39 @@ relayConnIdToMember cc name = do
|
||||
(Only connId : _) -> pure connId
|
||||
_ -> fail $ "no relay connection to member " <> T.unpack name
|
||||
|
||||
agentSendBody :: TestCC -> ByteString -> ByteString -> IO ()
|
||||
agentSendBody cc connId body = do
|
||||
let ChatController {smpAgent} = chatController cc
|
||||
runExceptT (sendMessages smpAgent [(connId, PQEncOff, MsgFlags False, vrValue body)]) >>= either (fail . show) (const $ pure ())
|
||||
|
||||
lastSndMsgFlags :: TestCC -> ByteString -> Int -> IO [T.Text]
|
||||
lastSndMsgFlags cc connId n = do
|
||||
rows <- withCCAgentTransaction cc $ \db ->
|
||||
DB.query
|
||||
db
|
||||
"SELECT msg_flags FROM messages WHERE conn_id = ? AND internal_snd_id IS NOT NULL AND msg_type = ? ORDER BY internal_id DESC LIMIT ?"
|
||||
(connId, Binary ("M" :: ByteString), n)
|
||||
pure $ reverse $ map fromOnly rows
|
||||
|
||||
lastJobNotification :: TestCC -> IO Bool
|
||||
lastJobNotification cc = do
|
||||
[Only (BI ntf)] <- withCCTransaction cc $ \db ->
|
||||
DB.query_ db "SELECT notification FROM delivery_jobs ORDER BY delivery_job_id DESC LIMIT 1"
|
||||
pure ntf
|
||||
|
||||
duplicateMessageEventsCfg :: IO (TVar [ContactName], ChatConfig)
|
||||
duplicateMessageEventsCfg = do
|
||||
dups <- newTVarIO []
|
||||
let hook _ evt = do
|
||||
case evt of
|
||||
Right (CEvtDuplicateGroupMessage _ GroupMember {localDisplayName}) -> atomically $ modifyTVar' dups (<> [localDisplayName])
|
||||
_ -> pure ()
|
||||
pure evt
|
||||
pure (dups, testCfg {chatHooks = defaultChatHooks {eventHook = Just hook}})
|
||||
|
||||
setMainAppOff :: TestCC -> IO ()
|
||||
setMainAppOff cc = atomically $ writeTVar (subscriptionMode $ chatController cc) SMOnlyCreate
|
||||
|
||||
itemSharedMsgId :: TestCC -> IO SharedMsgId
|
||||
itemSharedMsgId cc = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
|
||||
Reference in New Issue
Block a user