mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 17:25:42 +00:00
628b00eb08
* core: channel messages (WIP) * do not include member ID when quoting channel messages * query plans * reduce duplication * refactor * refactor plan * refactor 2 * all tests * remove plan * refactor 3 * refactor 4 * refactor 5 * refactor 6 * plans * plans to imrove test coverage and fix bugs * update plan * update plan * bug fixes (wip) * new plan * fixes wip * more tests * comment, fix lint * restore comment * restore comments * rename param * move type * simplify * comment * fix stale state * refactor * less diff * simplify * less diff * refactor --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
355 lines
17 KiB
Markdown
355 lines
17 KiB
Markdown
# Plan: Fix Channel Message Delivery Architecture
|
|
|
|
## Table of Contents
|
|
1. [Context](#context)
|
|
2. [Executive Summary](#executive-summary)
|
|
3. [Issue 1: Eliminate memberForChannel/memberIdForChannel](#issue-1)
|
|
4. [Issue 2: groupMsgReaction required GroupMember](#issue-2)
|
|
5. [Issue 3: Fix groupMessageUpdate lookup](#issue-3)
|
|
6. [Issue 4: DeliveryTaskContext type](#issue-4)
|
|
7. [Issue 5: Fix testChannelReactionAttribution](#issue-5)
|
|
8. [Issue 6: Fix testChannelUpdateFallbackSendAsGroup comment](#issue-6)
|
|
9. [Other: sendAsGroup parameter ordering](#other-issue)
|
|
10. [Verification](#verification)
|
|
|
|
## Context
|
|
|
|
The current implementation on `ep/channel-messages-2` determines delivery context (whether to forward messages as channel or as member) using `isChannelOwner` — inferring from the sender's role whether they're the channel owner. This is architecturally wrong: the delivery context should be determined **from the item's direction** (`CIChannelRcv` vs `CIGroupRcv`), not from who sent it. The `f/msg-from-channel` branch has the correct approach.
|
|
|
|
## Executive Summary
|
|
|
|
7 changes across 7 files:
|
|
1. **Delivery.hs** — Add `DeliveryTaskContext` type, update `NewMessageDeliveryTask` only (`MessageDeliveryTask` unchanged)
|
|
2. **Subscriber.hs** — Eliminate `isChannelOwner`/`memberForChannel`/`memberIdForChannel`; all processing functions return `Maybe DeliveryTaskContext`; determine `sentAsGroup` from item direction; `groupMsgReaction` takes required `GroupMember`; add `withAuthor` in forwarded handler
|
|
3. **Store/Delivery.hs** — Update SQL row mapping for `taskContext`
|
|
4. **Commands.hs** — Reorder `sendAsGroup` param in `APIForwardChatItems`
|
|
5. **Store/Messages.hs** — Reorder `showGroupAsSender` param in `createNewSndChatItem`
|
|
6. **Internal.hs** — Reorder `showGroupAsSender` param in `saveSndChatItems`, `prepareGroupMsg`
|
|
7. **Tests** — Fix reaction test comment/expectations, fix update fallback test comment
|
|
|
|
---
|
|
|
|
## Issue 1: Eliminate memberForChannel/memberIdForChannel {#issue-1}
|
|
|
|
**File:** `src/Simplex/Chat/Library/Subscriber.hs` lines 935-937, 939-991
|
|
|
|
**Problem:** `isChannelOwner`, `memberForChannel`, `memberIdForChannel` computed at lines 935-937 and passed to processing functions. This pre-infers delivery context from member role.
|
|
|
|
**Fix:** Remove these three bindings entirely. Always pass `(Just m'')` to functions that take `Maybe GroupMember`. Functions determine `sentAsGroup` from item direction internally.
|
|
|
|
**Direct handler changes (lines 939-991):**
|
|
```
|
|
-- BEFORE:
|
|
let isChannelOwner = useRelays' gInfo' && memberRole' m'' == GROwner
|
|
memberForChannel = if isChannelOwner then Nothing else Just m''
|
|
memberIdForChannel = memberId' <$> memberForChannel
|
|
(deliveryJobScope_, showGroupAsSender') <- case event of
|
|
...
|
|
forM deliveryJobScope_ $ \jobScope ->
|
|
pure $ NewMessageDeliveryTask {messageId = msgId, jobScope, showGroupAsSender = showGroupAsSender'}
|
|
|
|
-- AFTER:
|
|
deliveryTaskContext_ <- case event of
|
|
XMsgNew mc -> ... -- returns Maybe DeliveryTaskContext
|
|
XMsgFileDescr ... -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr
|
|
XMsgUpdate ... -> memberCanSend m'' msgScope Nothing $ groupMessageUpdate gInfo' (Just m'') sharedMsgId ...
|
|
XMsgDel ... -> groupMessageDelete gInfo' (Just m'') sharedMsgId ...
|
|
XMsgReact ... -> groupMsgReaction gInfo' m'' sharedMsgId ... -- required member
|
|
XFileCancel sharedMsgId -> xFileCancelGroup gInfo' (Just m'') sharedMsgId
|
|
...other events -> Just <$> memberEventDeliveryContext m'' / Nothing
|
|
forM deliveryTaskContext_ $ \taskContext ->
|
|
pure $ NewMessageDeliveryTask {messageId = msgId, taskContext}
|
|
```
|
|
|
|
**Processing function signature changes:**
|
|
- `groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> CM (Maybe DeliveryTaskContext)` — drop both `Maybe MemberId` params, pass `Maybe GroupMember`, determine `sentAsGroup` from `chatDir` of found item
|
|
- `groupMessageUpdate :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> ... -> Maybe Bool -> CM (Maybe DeliveryTaskContext)` — drop `senderGMId_` param
|
|
- `groupMessageDelete :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> ... -> CM (Maybe DeliveryTaskContext)` — drop `senderGMId_` param; fix `findOwnerCI` dual-lookup (lines 2028-2035) same as Issue 3: when `m_ = Nothing` search with `Nothing`, when `m_ = Just m` use member lookup directly
|
|
- `xFileCancelGroup :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> CM (Maybe DeliveryTaskContext)` — drop both `Maybe MemberId` params
|
|
|
|
**`validSender` simplification:** Remove second `Maybe MemberId` parameter. With `(Just m'')` always passed, validation is just:
|
|
```haskell
|
|
validSender :: Maybe MemberId -> CIDirection 'CTGroup 'MDRcv -> Bool
|
|
validSender (Just mId) (CIGroupRcv m) = sameMemberId mId m
|
|
validSender Nothing CIChannelRcv = True
|
|
validSender _ _ = False
|
|
```
|
|
|
|
**`isChannelDir` helper** remains as-is (line 1870-1872) — used to derive `sentAsGroup` from item's `chatDir`.
|
|
|
|
**`memberCanSend`** (line 1436): Generic signature `a -> CM a -> CM a` — no change needed. Default values at call sites change from `(Nothing, False)` to `Nothing`.
|
|
|
|
**`memberCanSend'`** (line 1448): Return type changes from `CM (Maybe DeliveryJobScope)` to `CM (Maybe DeliveryTaskContext)`. Used in forwarded handler (lines 3153, 3159).
|
|
|
|
---
|
|
|
|
## Issue 2: groupMsgReaction required GroupMember {#issue-2}
|
|
|
|
**File:** `src/Simplex/Chat/Library/Subscriber.hs` line 1814
|
|
|
|
**Problem:** `groupMsgReaction :: GroupInfo -> Maybe GroupMember -> ...` allows `Nothing`, uses `fromMaybe membership m_` fallback.
|
|
|
|
**Fix:** Change to required `GroupMember`:
|
|
```haskell
|
|
groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM (Maybe DeliveryTaskContext)
|
|
```
|
|
|
|
- No `reactor` binding needed — use `m` directly (eliminates `fromMaybe membership m_` fallback)
|
|
- `ciDir = CIGroupRcv (Just m)` (reactions always attributed to member)
|
|
- Always return `sentAsGroup = False` — reactions are never from channel
|
|
- Return type: `Maybe DeliveryTaskContext` (not tuple)
|
|
|
|
**Direct handler call site (line 958-960):**
|
|
```haskell
|
|
XMsgReact sharedMsgId memberId scope_ reaction add ->
|
|
groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs
|
|
```
|
|
|
|
**Forwarded handler call site (line 3162-3163):**
|
|
```haskell
|
|
XMsgReact sharedMsgId memId_ scope_ reaction add ->
|
|
withAuthor XMsgReact_ $ \author -> groupMsgReaction gInfo author sharedMsgId memId_ scope_ reaction add rcvMsg msgTs
|
|
```
|
|
|
|
---
|
|
|
|
## Issue 3: Fix groupMessageUpdate lookup {#issue-3}
|
|
|
|
**File:** `src/Simplex/Chat/Library/Subscriber.hs` lines 1973-1994
|
|
|
|
**Problem:** Dual-lookup with `catchError` tries `Nothing` first, then falls back to `senderGMId_`. This is wrong — the `asGroup_` flag from XMsgUpdate should drive the search.
|
|
|
|
**Fix:** Use `asGroup_` (the wire flag) to determine search strategy. No `senderGMId_` parameter needed:
|
|
```haskell
|
|
updateRcvChatItem = do
|
|
(cci, scopeInfo) <- withStore $ \db -> do
|
|
cci <- case m_ of
|
|
Just m -> getGroupMemberCIBySharedMsgId db user gInfo (memberId' m) sharedMsgId
|
|
Nothing -> getGroupChatItemBySharedMsgId db user gInfo Nothing sharedMsgId
|
|
(cci,) <$> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci)
|
|
```
|
|
|
|
When `m_ = Nothing` (channel owner as channel), search with `Nothing` group_member_id → finds channel items.
|
|
When `m_ = Just m` (attributed member message), search with member's `memberId` → finds member items.
|
|
|
|
The `isSender` check also simplifies — just check `m_` matches the found item's member.
|
|
|
|
**Fallback path** (lines 1948-1968, `catchCINotFound`): When item not found, `showGroupAsSender` is derived from `asGroup_` flag (or defaults based on `m_`), which maps to `sentAsGroup` in the `DeliveryTaskContext`.
|
|
|
|
---
|
|
|
|
## Issue 4: DeliveryTaskContext type {#issue-4}
|
|
|
|
**File:** `src/Simplex/Chat/Delivery.hs`
|
|
|
|
### 4a. Add DeliveryTaskContext type
|
|
```haskell
|
|
data DeliveryTaskContext = DeliveryTaskContext
|
|
{ jobScope :: DeliveryJobScope,
|
|
sentAsGroup :: ShowGroupAsSender
|
|
}
|
|
deriving (Show)
|
|
```
|
|
|
|
Uses existing `type ShowGroupAsSender = Bool` from Messages.hs.
|
|
|
|
### 4b. Modify existing helpers
|
|
Rename `infoToDeliveryScope` → `infoToDeliveryContext`, inline the scope logic, add `ShowGroupAsSender` parameter:
|
|
```haskell
|
|
infoToDeliveryContext :: GroupInfo -> Maybe GroupChatScopeInfo -> ShowGroupAsSender -> DeliveryTaskContext
|
|
infoToDeliveryContext GroupInfo {membership} scopeInfo sentAsGroup = DeliveryTaskContext {jobScope, sentAsGroup}
|
|
where
|
|
jobScope = case scopeInfo of
|
|
Nothing -> DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
|
|
Just GCSIMemberSupport {groupMember_} ->
|
|
let supportGMId = groupMemberId' $ fromMaybe membership groupMember_
|
|
in DJSMemberSupport {supportGMId}
|
|
```
|
|
Remove `infoToDeliveryScope` entirely.
|
|
|
|
Rename `memberEventDeliveryScope` → `memberEventDeliveryContext`, change return type:
|
|
```haskell
|
|
memberEventDeliveryContext :: GroupMember -> Maybe DeliveryTaskContext
|
|
memberEventDeliveryContext m@GroupMember {memberRole, memberStatus}
|
|
| memberStatus == GSMemPendingApproval = Nothing
|
|
| memberStatus == GSMemPendingReview = Just $ DeliveryTaskContext {jobScope = DJSMemberSupport {supportGMId = groupMemberId' m}, sentAsGroup = False}
|
|
| memberRole >= GRModerator = Just $ DeliveryTaskContext {jobScope = DJSGroup {jobSpec = DJDeliveryJob {includePending = True}}, sentAsGroup = False}
|
|
| otherwise = Just $ DeliveryTaskContext {jobScope = DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}, sentAsGroup = False}
|
|
```
|
|
|
|
### 4c. Update NewMessageDeliveryTask
|
|
```haskell
|
|
data NewMessageDeliveryTask = NewMessageDeliveryTask
|
|
{ messageId :: MessageId,
|
|
taskContext :: DeliveryTaskContext
|
|
}
|
|
deriving (Show)
|
|
```
|
|
|
|
### 4d. MessageDeliveryTask — no change
|
|
|
|
`MessageDeliveryTask` stays as-is. It's constructed from DB rows in `getMsgDeliveryTask_` and consumed by relay forwarding code — those consumers need `jobScope` and `fwdSender` directly, not `DeliveryTaskContext`. `DeliveryTaskContext` is only for the path from processing functions → `NewMessageDeliveryTask` creation.
|
|
|
|
### 4e. Update Store/Delivery.hs
|
|
|
|
**`createMsgDeliveryTask`** (line 71-87): Extract `jobScope` and `sentAsGroup` from `taskContext` instead of separate `jobScope`/`showGroupAsSender` fields.
|
|
|
|
**`getMsgDeliveryTask_`** — no change needed (`MessageDeliveryTask` unchanged).
|
|
|
|
### 4f. Consumers of MessageDeliveryTask — no change needed
|
|
|
|
**Subscriber.hs** lines ~3325-3333 and **Messages/Batch.hs** lines ~77-80 already pattern match on `FwdSender` and use `jobScope` from `MessageDeliveryTask`. Since `MessageDeliveryTask` is unchanged, no updates needed.
|
|
|
|
### 4g. Return type changes in processing functions
|
|
|
|
All functions currently returning `(Maybe DeliveryJobScope, ShowGroupAsSender)` change to `Maybe DeliveryTaskContext`:
|
|
- `groupMessageFileDescription` → `CM (Maybe DeliveryTaskContext)`
|
|
- `groupMessageUpdate` → `CM (Maybe DeliveryTaskContext)`
|
|
- `groupMessageDelete` → `CM (Maybe DeliveryTaskContext)`
|
|
- `xFileCancelGroup` → `CM (Maybe DeliveryTaskContext)`
|
|
- `groupMsgReaction` → `CM (Maybe DeliveryTaskContext)`
|
|
|
|
Events that return `(Nothing, False)` or `(Just scope, False)` are updated:
|
|
- `(Nothing, False)` → `Nothing`
|
|
- `(Just scope, False)` → `Just $ DeliveryTaskContext scope False` (or use `memberEventDeliveryContext`)
|
|
- `(Just scope, showGroupAsSender)` → `Just $ DeliveryTaskContext scope showGroupAsSender` (or use `infoToDeliveryContext`)
|
|
|
|
---
|
|
|
|
## Issue 5: Fix testChannelReactionAttribution {#issue-5}
|
|
|
|
**File:** `tests/ChatTests/Groups.hs` lines 9057-9084
|
|
|
|
**Problem:** Comment says "reaction is forwarded as channel (owner is anonymous)" and expects `#team>`. Owner should react **as member** — reactions are always `sentAsGroup = False`.
|
|
|
|
**Fix:** Change comment and expectations:
|
|
```haskell
|
|
-- owner reacts to own member message - reaction is forwarded as member
|
|
alice ##> "+1 #team hello"
|
|
alice <## "added 👍"
|
|
bob <# "#team alice> > alice hello"
|
|
bob <## " + 👍"
|
|
concurrentlyN_
|
|
[ do cath <# "#team alice> > alice hello"
|
|
cath <## " + 👍",
|
|
do dan <# "#team alice> > alice hello"
|
|
dan <## " + 👍",
|
|
do eve <# "#team alice> > alice hello"
|
|
eve <## " + 👍"
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Issue 6: Fix testChannelUpdateFallbackSendAsGroup comment {#issue-6}
|
|
|
|
**File:** `tests/ChatTests/Groups.hs` line 9127
|
|
|
|
**Problem:** Comment says "bob's internally deleted item is still in DB, update finds it with correct member direction". This is wrong — the item was internally deleted, then XMsgUpdate re-creates it via the `catchCINotFound` fallback.
|
|
|
|
**Fix:** Change comment to:
|
|
```haskell
|
|
-- bob's internally deleted item is re-created as from member (sendAsGroup=False)
|
|
```
|
|
|
|
---
|
|
|
|
## Other: sendAsGroup parameter ordering {#other-issue}
|
|
|
|
**Problem:** `sendAsGroup`/`ShowGroupAsSender` should come right after direction/scope, not at the end.
|
|
|
|
### 7a. `APIForwardChatItems` constructor
|
|
|
|
**File:** `src/Simplex/Chat/Library/Commands.hs` (ChatCommand type definition + parser)
|
|
|
|
Current: `APIForwardChatItems toChat fromChat itemIds itemTTL sendAsGroup`
|
|
New: `APIForwardChatItems toChat sendAsGroup fromChat itemIds itemTTL`
|
|
|
|
Affects:
|
|
- Constructor definition in `src/Simplex/Chat/Controller.hs` line 341
|
|
- Parser at line 4639
|
|
- Call sites at lines 930, 2192, 2198, 2204
|
|
|
|
### 7b. `createNewSndChatItem`
|
|
|
|
**File:** `src/Simplex/Chat/Store/Messages.hs` line 528
|
|
|
|
Current: `createNewSndChatItem db user chatDirection msg ciContent quotedItem itemForwarded timed live hasLink showGroupAsSender createdAt`
|
|
New: `createNewSndChatItem db user chatDirection showGroupAsSender msg ciContent quotedItem itemForwarded timed live hasLink createdAt`
|
|
|
|
Move `showGroupAsSender` right after `chatDirection` (direction context).
|
|
|
|
Affects call site in `Internal.hs` line 2276.
|
|
|
|
### 7c. `saveSndChatItems`
|
|
|
|
**File:** `src/Simplex/Chat/Library/Internal.hs` line 2256-2265
|
|
|
|
Current param order: `user -> cd -> itemsData -> itemTimed -> live -> showGroupAsSender`
|
|
New: `user -> cd -> showGroupAsSender -> itemsData -> itemTimed -> live`
|
|
|
|
Move `showGroupAsSender` right after `cd` (direction context).
|
|
|
|
Affects call sites: Internal.hs line 2242, Commands.hs lines 2561, 2608 (and the `saveSndChatItem'` wrapper at line 2240).
|
|
|
|
### 7d. `prepareGroupMsg`
|
|
|
|
**File:** `src/Simplex/Chat/Library/Internal.hs` line 203
|
|
|
|
Current: `prepareGroupMsg db user gInfo msgScope mc mentions quotedItemId_ itemForwarded fInv_ timed_ live showGroupAsSender`
|
|
New: `prepareGroupMsg db user gInfo msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live`
|
|
|
|
Move `showGroupAsSender` right after `msgScope` (scope context).
|
|
|
|
Affects call sites: Internal.hs line 1249, Commands.hs line 4094.
|
|
|
|
---
|
|
|
|
## Forwarded handler (xGrpMsgForward) changes
|
|
|
|
**File:** `src/Simplex/Chat/Library/Subscriber.hs` lines 3136-3173
|
|
|
|
Add `withAuthor` helper to replace ad-hoc `| Just author <- author_` guards:
|
|
```haskell
|
|
where
|
|
withAuthor :: CMEventTag e -> (GroupMember -> CM ()) -> CM ()
|
|
withAuthor tag action = case author_ of
|
|
Just author -> action author
|
|
Nothing -> messageError $ "x.grp.msg.forward: event " <> tshow tag <> " requires author"
|
|
```
|
|
|
|
Update forwarded event handling:
|
|
- `XMsgFileDescr` → pass `author_` (Maybe GroupMember) directly
|
|
- `XMsgUpdate` → pass `author_` directly, void result
|
|
- `XMsgDel` → pass `author_` directly, void result
|
|
- `XMsgReact` → use `withAuthor` (required member)
|
|
- `XFileCancel` → pass `author_` directly
|
|
- Other events with `| Just author <- author_` → use `withAuthor`
|
|
|
|
---
|
|
|
|
## Files Modified
|
|
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `src/Simplex/Chat/Delivery.hs` | Add `DeliveryTaskContext`, update `NewMessageDeliveryTask` only |
|
|
| `src/Simplex/Chat/Store/Delivery.hs` | Update `createMsgDeliveryTask` to extract from `taskContext` |
|
|
| `src/Simplex/Chat/Library/Subscriber.hs` | Eliminate `isChannelOwner`/`memberForChannel`/`memberIdForChannel`; change function signatures to return `Maybe DeliveryTaskContext`; add `withAuthor`; simplify `validSender`; `groupMsgReaction` required member; fix lookup |
|
|
| `src/Simplex/Chat/Controller.hs` | Reorder `sendAsGroup` in `APIForwardChatItems` constructor |
|
|
| `src/Simplex/Chat/Library/Commands.hs` | Reorder `sendAsGroup` in `APIForwardChatItems` parser + call sites |
|
|
| `src/Simplex/Chat/Store/Messages.hs` | Reorder `showGroupAsSender` in `createNewSndChatItem` |
|
|
| `src/Simplex/Chat/Library/Internal.hs` | Reorder `showGroupAsSender` in `saveSndChatItems`, `prepareGroupMsg` |
|
|
| `src/Simplex/Chat/Messages/Batch.hs` | No change needed (`MessageDeliveryTask` unchanged) |
|
|
| `tests/ChatTests/Groups.hs` | Fix reaction test expectations + update fallback comment |
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
1. `cabal build --ghc-options=-O0` — must compile clean
|
|
2. Run channel test suite: `cabal test simplex-chat-test --test-option='-m "channels"' --ghc-options=-O0`
|
|
3. Adversarial self-review loop until 2 consecutive clean passes
|
|
4. Verify no `isChannelOwner` references remain in Subscriber.hs direct handler
|
|
5. Verify `groupMsgReaction` signature has required `GroupMember` (no Maybe)
|
|
6. Verify no dual-lookup with `catchError` in `groupMessageUpdate`
|