mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-09 16:41:47 +00:00
wip
This commit is contained in:
@@ -2247,14 +2247,18 @@ struct ChatView: View {
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
itemId: ci.id,
|
||||
reactionCount: r,
|
||||
selectedMember: $selectedMember,
|
||||
profileRadius: profileRadius
|
||||
)
|
||||
if groupInfo.useRelays && !groupInfo.isOwner {
|
||||
v
|
||||
} else {
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
itemId: ci.id,
|
||||
reactionCount: r,
|
||||
selectedMember: $selectedMember,
|
||||
profileRadius: profileRadius
|
||||
)
|
||||
}
|
||||
}
|
||||
case let .direct(contact):
|
||||
v.contextMenu {
|
||||
|
||||
@@ -124,6 +124,8 @@ Add `subscriber_body` to the INSERT. Existing call sites (2 in Subscriber.hs tas
|
||||
|
||||
Update `MessageDeliveryJobRow`, `getNextDeliveryJob` query, and `toDeliveryJob` to read `subscriber_body` column into the new `subscriberBody_` field.
|
||||
|
||||
In `getNextDeliveryTasks`, add sender filtering for DJReaction tasks on relay groups (via `isSenderFiltered` helper). This ensures `singleSenderGMId_` is always Just for DJReaction jobs, preventing subscribers from receiving their own reaction back via FwdChannel in multi-sender batches.
|
||||
|
||||
### 3. DB migration
|
||||
|
||||
```sql
|
||||
@@ -364,7 +366,7 @@ Reaction pills (emoji + count) remain visible. Only the long-press context menu
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/Simplex/Chat/Delivery.hs` | Add `DJReaction` to `DeliveryJobSpec`/tag; add `subscriberBody_` to `MessageDeliveryJob` |
|
||||
| `src/Simplex/Chat/Store/Delivery.hs` | `jobScopeRow_`/`toJobScope_` for new tag; `createMsgDeliveryJob` new parameter; `getNextDeliveryJob` reads new column |
|
||||
| `src/Simplex/Chat/Store/Delivery.hs` | `jobScopeRow_`/`toJobScope_` for new tag; `createMsgDeliveryJob` new parameter; `getNextDeliveryJob` reads new column; `getNextDeliveryTasks` sender-filters DJReaction |
|
||||
| `src/Simplex/Chat/Library/Subscriber.hs` | `groupMsgReaction` accepts `Maybe GroupMember`, adds channel reaction path, returns `DJReaction` for channels; task/job workers handle `DJReaction`; `processForwardedMsg` passes `author_` directly |
|
||||
| `src/Simplex/Chat/Store/Messages.hs` | New `setGroupReactionNoMember` |
|
||||
| `src/Simplex/Chat/Library/Commands.hs` | `APIGetReactionMembers` restricted for non-owner channel members |
|
||||
@@ -375,40 +377,52 @@ Reaction pills (emoji + count) remain visible. Only the long-press context menu
|
||||
|
||||
### Pass 1
|
||||
|
||||
1. **Owner duplicate delivery**: Owner is a member. In the cursor loop, they're partitioned into the `owners` group and receive `body` (FwdMember). They do NOT receive `subscriberBody`. No duplicate reactions.
|
||||
1. **Owner duplicate delivery**: Owner partitioned into `owners` group in `sendBodyToMembersReaction` via `memberRole' m >= GROwner`. Receives `body` (FwdMember) only. No subscriber body sent to owners.
|
||||
|
||||
2. **Subscriber's own reaction**: The reacting subscriber is excluded from delivery via `singleSenderGMId_` in `getGroupMembersByCursor` (`AND group_member_id IS DISTINCT FROM ?`). No self-delivery, no double-counting.
|
||||
2. **Subscriber's own reaction excluded**: `isSenderFiltered` in `getNextDeliveryTasks` ensures DJReaction tasks are always sender-filtered (even for relay groups). All tasks in a DJReaction batch are from the same sender, so `singleSenderGMId_` is always Just. `getGroupMembersByCursor` excludes the sender via `AND group_member_id IS DISTINCT FROM ?`.
|
||||
|
||||
3. **Owner's reaction identity hidden from subscribers**: When an owner reacts, the relay processes it and returns `DJReaction`. The subscriber body uses `FwdChannel`, hiding the owner's identity from subscribers. Owners see each other's identities via `body`. Correct.
|
||||
3. **Multi-sender batch dedup** (found and fixed during review): Without sender filtering, if multiple senders' DJReaction tasks were batched, `singleSenderGMId_` would be Nothing and senders would receive their own reactions back as FwdChannel, causing duplicate NULL-member reaction rows. Fixed by adding `isSenderFiltered` to always use sender-filtered query for DJReaction. Tasks from other senders remain DTSNew and are processed in subsequent iterations.
|
||||
|
||||
4. **Member support scope reactions**: The member support scope branch in `groupMsgReaction` (line 1929–1934) returns `DJSMemberSupport`, an entirely different delivery scope processed by a different job worker path. Unaffected.
|
||||
4. **Owner's reaction identity hidden from subscribers**: Owner reacts → relay returns DJReaction → subscriber body uses FwdChannel → subscribers see only emoji + count, no member identity.
|
||||
|
||||
5. **Non-channel groups**: `groupMsgReaction` returns `DJReaction` only when `useRelays' g`. Regular groups continue using `DJDeliveryJob`. Unaffected.
|
||||
5. **Non-channel groups unaffected**: `DJReaction` only returned when `useRelays' g`. Regular groups use `DJDeliveryJob`. `isSenderFiltered` returns False for non-DJReaction scopes.
|
||||
|
||||
6. **Batching**: `getNextDeliveryTasks` filters by `jobScopeRow_`. `DJReaction` tasks have tag `"reaction"` — they batch with each other, NOT with `DJDeliveryJob` tasks. Multiple rapid reactions are correctly batched together, and both owner and subscriber bodies contain all batched elements.
|
||||
6. **Member support scope unaffected**: Returns `DJSMemberSupport`, different delivery scope and worker. Never interacts with DJReaction.
|
||||
|
||||
7. **`getGroupCIReactions` counts NULL-member reactions**: The aggregation query groups by reaction and counts `chat_item_reaction_id` with no filter on `group_member_id`. NULL-member rows are counted. `userReacted = MAX(reaction_sent)` — for received anonymous reactions `reaction_sent = 0`, for the user's own reaction `reaction_sent = 1`. Correct.
|
||||
7. **Batching**: DJReaction tasks batch same-scope AND same-sender. Tag `"reaction"` separates from `DJDeliveryJob`. Both owner and subscriber bodies encode the same set of tasks.
|
||||
|
||||
8. **`getReactionMembers` on owner's device**: On the owner's device, reactions arrive via `FwdMember` and are stored with real `group_member_id` values. `getReactionMembers` fetches by `group_member_id` and joins `group_members` — works as before. On the subscriber's device, `APIGetReactionMembers` returns empty (API restriction), so NULL-member reactions are never queried for member details.
|
||||
8. **`getGroupCIReactions` counts NULL-member reactions**: Aggregation groups by reaction, counts all rows. `userReacted = MAX(reaction_sent)` — 0 for anonymous, 1 for user's own.
|
||||
|
||||
9. **Reaction removal (add=False)**: Relay processes removal, returns `DJReaction`. Task/job workers encode both bodies. Owner receives `FwdMember` removal — `setGroupReaction` deletes the specific member's reaction row. Subscriber receives `FwdChannel` removal — `groupMsgReaction` `Nothing` branch calls `setGroupReactionNoMember`, which deletes one NULL-member row via `LIMIT 1`. Correct.
|
||||
9. **API restriction consistent with UI**: Both use `< GROwner` threshold. `APIGetReactionMembers` returns `[]` for non-owner channel subscribers. iOS suppresses context menu.
|
||||
|
||||
10. **`saveGroupFwdRcvMsg` with `Nothing` author**: Already works — `refAuthorId = groupMemberId' <$> refAuthorMember_` handles `Nothing` (Internal.hs:2283). Dedup via `sharedMsgId_` is unaffected.
|
||||
10. **Reaction removal (add=False)**: `setGroupReactionNoMember` DELETE uses LIMIT 1 subquery. Finds one NULL-member row or no-ops if none exists.
|
||||
|
||||
11. **`createMsgDeliveryJob` call sites**: Three existing call sites (Subscriber.hs:3531, 3548; Commands.hs:2927) need `Nothing` as the new subscriber body parameter. Mechanical change.
|
||||
11. **`processForwardedMsg` passes `author_` directly**: FwdMember → Just author → `groupMsgReaction (Just m)`. FwdChannel → Nothing → `groupMsgReaction Nothing`. Matches sibling handlers.
|
||||
|
||||
12. **Migration**: `subscriber_body` column nullable. Existing rows get NULL. `toDeliveryJob` reads as `Maybe (Binary ByteString)`.
|
||||
|
||||
No issues found.
|
||||
|
||||
### Pass 2
|
||||
|
||||
12. **Subscriber body encoding size**: `FwdChannel` encodes as `"C"` (1 byte) vs `FwdMember` which is `"M" + memberId + memberName` (variable, larger). If the owner body fits within `maxEncodedMsgLength`, the subscriber body will also fit (same content, smaller wrapper). No overflow risk.
|
||||
13. **`isSenderFiltered` correctness**: Only matches `DJSGroup {jobSpec = DJReaction}`. DJDeliveryJob and DJRelayRemoved on relay groups continue without sender filter. DJRelayRemoved doesn't use `getNextDeliveryTasks` (single-task processing). Only DJDeliveryJob and DJReaction go through the plural version. Behavior preserved for all existing paths.
|
||||
|
||||
13. **`catchCINotFound` in `groupMsgReaction` `Nothing` branch**: When the chat item doesn't exist yet, the fallback stores the reaction with NULL member via `setGroupReactionNoMember`. When the chat item eventually arrives, `getGroupCIReactions` picks up all stored reactions including NULL-member ones. Counts are correct.
|
||||
14. **Subscriber body encoding includes signatures**: For signed messages, `encodeFwdElement` includes signature bytes. On receiving side, FwdChannel path discards them with `VMUnsigned chatMsg`. Extra bytes but functionally correct. No information leak.
|
||||
|
||||
14. **`CIChannelRcv` direction in reaction UI event**: `CIReaction` uses `chatDir :: CIDirection c d`. `CIChannelRcv` is a valid `CIDirection 'CTGroup 'MDRcv`. The iOS event handler (`.chatItemReaction`) just calls `m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)` — it updates the chat item's reaction counts regardless of direction. No UI issue.
|
||||
15. **`partition` threshold `>= GROwner`**: Only GROwner gets owner body. GRAdmin, GRModerator, GRMember, GRObserver get subscriber body. Correct for channel privacy model.
|
||||
|
||||
15. **Old client compatibility**: Old clients that receive `FwdChannel` + `XMsgReact` parse `FwdChannel` correctly (it's an existing encoding). They still use `withAuthor XMsgReact_` → error logged, reaction silently dropped. Acceptable degradation — no crash, reaction count is marginally lower until client update.
|
||||
16. **Subscriber body encoding size**: FwdChannel encodes smaller than FwdMember. If owner body fits in `maxEncodedMsgLength`, subscriber body fits too.
|
||||
|
||||
16. **`subscriberBody_` for non-`DJReaction` jobs**: The field is `Maybe ByteString`, defaults to `Nothing` for all other job types. The `fromMaybe body subscriberBody_` fallback in the job worker ensures non-owners get `body` if no subscriber body exists. Safe.
|
||||
17. **`catchCINotFound` fallback in `Nothing` branch**: Stores reaction without UI event when chat item doesn't exist. Matches `Just m` branch fallback behavior.
|
||||
|
||||
18. **`CIChannelRcv` direction in reaction UI event**: Valid `CIDirection 'CTGroup 'MDRcv`. iOS event handler updates reaction counts regardless of direction.
|
||||
|
||||
19. **Old client compatibility**: FwdChannel is existing encoding. Old clients hit `withAuthor XMsgReact_` → error logged, reaction dropped. No crash. Acceptable degradation.
|
||||
|
||||
20. **`subscriberBody_` for non-`DJReaction` jobs**: `Maybe ByteString`, defaults to `Nothing`. `fromMaybe body subscriberBody_` fallback safe.
|
||||
|
||||
21. **Column mapping**: SELECT lists 9 columns matching `MessageDeliveryJobRow` type. `subscriber_body` at position 8 maps to `Maybe (Binary ByteString)`.
|
||||
|
||||
22. **`sentAsGroup = False` for DJReaction tasks**: Task worker reads `showGroupAsSender = False` → `fwdSender = FwdMember`. Owner body has member identity. Subscriber body explicitly overrides with FwdChannel.
|
||||
|
||||
No issues found. Two consecutive clean passes.
|
||||
|
||||
@@ -130,6 +130,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260430_subscriber_body
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -282,6 +283,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260430_subscriber_body
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
@@ -59,11 +59,13 @@ data DeliveryJobScope
|
||||
|
||||
data DeliveryJobSpec
|
||||
= DJDeliveryJob {includePending :: Bool}
|
||||
| DJReaction
|
||||
| DJRelayRemoved
|
||||
deriving (Show)
|
||||
|
||||
data DeliveryJobSpecTag
|
||||
= DJSTDeliveryJob
|
||||
| DJSTReaction
|
||||
| DJSTRelayRemoved
|
||||
deriving (Show)
|
||||
|
||||
@@ -74,10 +76,12 @@ instance ToField DeliveryJobSpecTag where toField = toField . textEncode
|
||||
instance TextEncoding DeliveryJobSpecTag where
|
||||
textDecode = \case
|
||||
"delivery_job" -> Just DJSTDeliveryJob
|
||||
"reaction" -> Just DJSTReaction
|
||||
"relay_removed" -> Just DJSTRelayRemoved
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
DJSTDeliveryJob -> "delivery_job"
|
||||
DJSTReaction -> "reaction"
|
||||
DJSTRelayRemoved -> "relay_removed"
|
||||
|
||||
toWorkerScope :: DeliveryJobScope -> DeliveryWorkerScope
|
||||
@@ -101,6 +105,7 @@ jobScopeImpliedSpec = \case
|
||||
jobSpecImpliedPending :: DeliveryJobSpec -> Bool
|
||||
jobSpecImpliedPending = \case
|
||||
DJDeliveryJob {includePending} -> includePending
|
||||
DJReaction -> False
|
||||
DJRelayRemoved -> True
|
||||
|
||||
infoToDeliveryContext :: GroupInfo -> Maybe GroupChatScopeInfo -> ShowGroupAsSender -> DeliveryTaskContext
|
||||
@@ -163,6 +168,7 @@ data MessageDeliveryJob = MessageDeliveryJob
|
||||
jobScope :: DeliveryJobScope,
|
||||
singleSenderGMId_ :: Maybe GroupMemberId, -- Just for single-sender deliveries, Nothing for multi-sender deliveries
|
||||
body :: ByteString,
|
||||
subscriberBody_ :: Maybe ByteString,
|
||||
cursorGMId_ :: Maybe GroupMemberId
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -882,9 +882,13 @@ processChatCommand vr nm = \case
|
||||
when (add && length rs >= maxMsgReactions) $
|
||||
throwCmdError "too many reactions"
|
||||
APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do
|
||||
memberReactions <- withStore $ \db -> do
|
||||
CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId
|
||||
liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction
|
||||
gInfo <- withStore $ \db -> getGroupInfo db vr user groupId
|
||||
memberReactions <-
|
||||
if useRelays' gInfo && memberRole' (membership gInfo) < GROwner
|
||||
then pure []
|
||||
else withStore $ \db -> do
|
||||
CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId
|
||||
liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction
|
||||
pure $ CRReactionMembers user memberReactions
|
||||
-- TODO [knocking] forward from scope?
|
||||
APIPlanForwardChatItems (ChatRef fromCType fromChatId _scope) itemIds -> withUser $ \user -> case fromCType of
|
||||
@@ -2924,7 +2928,7 @@ processChatCommand vr nm = \case
|
||||
withFastStore' $ \db -> do
|
||||
deleteGroupDeliveryTasks db gInfo
|
||||
deleteGroupDeliveryJobs db gInfo
|
||||
createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) Nothing body
|
||||
createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) Nothing body Nothing
|
||||
lift . void $ getDeliveryJobWorker True (groupId, DWSGroup)
|
||||
pure msg
|
||||
leaveGroupSendMsg user gInfo = do
|
||||
|
||||
@@ -28,7 +28,7 @@ import Data.Either (lefts, partitionEithers, rights)
|
||||
import Data.Foldable (foldr', foldrM)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List (find, partition)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
@@ -995,7 +995,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
memberCanSend (Just m'') msgScope $
|
||||
groupMessageUpdate gInfo' (Just m'') sharedMsgId mContent mentions msgScope msg brokerTs ttl live asGroup_
|
||||
XMsgDel sharedMsgId memberId_ scope_ -> groupMessageDelete gInfo' (Just m'') sharedMsgId memberId_ scope_ msg brokerTs
|
||||
XMsgReact sharedMsgId memberId scope_ reaction add -> groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs
|
||||
XMsgReact sharedMsgId memberId scope_ reaction add -> groupMsgReaction gInfo' (Just m'') sharedMsgId memberId scope_ reaction add msg brokerTs
|
||||
-- TODO discontinue XFile
|
||||
XFile fInv -> Nothing <$ processGroupFileInvitation' gInfo' m'' fInv msg brokerTs
|
||||
XFileCancel sharedMsgId -> xFileCancelGroup gInfo' (Just m'') sharedMsgId
|
||||
@@ -1919,27 +1919,48 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
else pure Nothing
|
||||
mapM_ toView cEvt_
|
||||
|
||||
groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM (Maybe DeliveryTaskContext)
|
||||
groupMsgReaction g m sharedMsgId itemMemberId scope_ reaction add RcvMessage {msgId} brokerTs
|
||||
| groupFeatureAllowed SGFReactions g = do
|
||||
rs <- withStore' $ \db -> getGroupReactions db g m itemMemberId sharedMsgId False
|
||||
if reactionAllowed add reaction rs
|
||||
then
|
||||
updateChatItemReaction `catchCINotFound` \_ -> case scope_ of
|
||||
Just (MSMember scopeMemberId)
|
||||
| memberRole' m >= GRModerator || scopeMemberId == memberId' m -> do
|
||||
djScope <- withStore $ \db -> do
|
||||
liftIO $ setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
|
||||
Just . DJSMemberSupport <$> getScopeMemberIdViaMemberId db user g m scopeMemberId
|
||||
pure $ fmap (\js -> DeliveryTaskContext js False) djScope
|
||||
| otherwise -> pure Nothing
|
||||
Nothing -> do
|
||||
withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
|
||||
pure $ Just $ DeliveryTaskContext (DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}) False
|
||||
else pure Nothing
|
||||
groupMsgReaction :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM (Maybe DeliveryTaskContext)
|
||||
groupMsgReaction g m_ sharedMsgId itemMemberId scope_ reaction add RcvMessage {msgId} brokerTs
|
||||
| groupFeatureAllowed SGFReactions g = case m_ of
|
||||
Nothing ->
|
||||
updateChannelReaction `catchCINotFound` \_ ->
|
||||
withStore' (\db -> setGroupReactionNoMember db g itemMemberId sharedMsgId reaction add msgId brokerTs)
|
||||
$> Nothing
|
||||
Just m -> do
|
||||
rs <- withStore' $ \db -> getGroupReactions db g m itemMemberId sharedMsgId False
|
||||
if reactionAllowed add reaction rs
|
||||
then
|
||||
updateChatItemReaction m `catchCINotFound` \_ -> case scope_ of
|
||||
Just (MSMember scopeMemberId)
|
||||
| memberRole' m >= GRModerator || scopeMemberId == memberId' m -> do
|
||||
djScope <- withStore $ \db -> do
|
||||
liftIO $ setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
|
||||
Just . DJSMemberSupport <$> getScopeMemberIdViaMemberId db user g m scopeMemberId
|
||||
pure $ fmap (\js -> DeliveryTaskContext js False) djScope
|
||||
| otherwise -> pure Nothing
|
||||
Nothing -> do
|
||||
withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
|
||||
let jobSpec = if useRelays' g then DJReaction else DJDeliveryJob {includePending = False}
|
||||
pure $ Just $ DeliveryTaskContext (DJSGroup {jobSpec}) False
|
||||
else pure Nothing
|
||||
| otherwise = pure Nothing
|
||||
where
|
||||
updateChatItemReaction = do
|
||||
updateChannelReaction = do
|
||||
(CChatItem md ci, scopeInfo) <- withStore $ \db -> do
|
||||
cci <- case itemMemberId of
|
||||
Just itemMemberId' -> getGroupMemberCIBySharedMsgId db user g itemMemberId' sharedMsgId
|
||||
Nothing -> getGroupChatItemBySharedMsgId db user g Nothing sharedMsgId
|
||||
scopeInfo <- getGroupChatScopeInfoForItem db vr user g (cChatItemId cci)
|
||||
pure (cci, scopeInfo)
|
||||
when (ciReactionAllowed ci) $ do
|
||||
reactions <- withStore' $ \db -> do
|
||||
setGroupReactionNoMember db g itemMemberId sharedMsgId reaction add msgId brokerTs
|
||||
getGroupCIReactions db g itemMemberId sharedMsgId
|
||||
let ci' = CChatItem md ci {reactions}
|
||||
r = ACIReaction SCTGroup SMDRcv (GroupChat g scopeInfo) $ CIReaction CIChannelRcv ci' brokerTs reaction
|
||||
toView $ CEvtChatItemReaction user add r
|
||||
pure Nothing
|
||||
updateChatItemReaction m = do
|
||||
(CChatItem md ci, scopeInfo) <- withStore $ \db -> do
|
||||
cci <- case itemMemberId of
|
||||
Just itemMemberId' -> getGroupMemberCIBySharedMsgId db user g itemMemberId' sharedMsgId
|
||||
@@ -1954,7 +1975,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let ci' = CChatItem md ci {reactions}
|
||||
r = ACIReaction SCTGroup SMDRcv (GroupChat g scopeInfo) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction
|
||||
toView $ CEvtChatItemReaction user add r
|
||||
pure $ Just $ infoToDeliveryContext g scopeInfo False
|
||||
pure $ Just $ case scopeInfo of
|
||||
Nothing | useRelays' g -> DeliveryTaskContext (DJSGroup {jobSpec = DJReaction}) False
|
||||
_ -> infoToDeliveryContext g scopeInfo False
|
||||
else pure Nothing
|
||||
|
||||
reactionAllowed :: Bool -> MsgReaction -> [MsgReaction] -> Bool
|
||||
@@ -3346,7 +3369,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
|
||||
void $ memberCanSend author_ msgScope $ groupMessageUpdate gInfo author_ sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live asGroup_
|
||||
XMsgDel sharedMsgId memId scope_ -> void $ groupMessageDelete gInfo author_ sharedMsgId memId scope_ rcvMsg msgTs
|
||||
XMsgReact sharedMsgId memId scope_ reaction add -> withAuthor XMsgReact_ $ \author -> void $ groupMsgReaction gInfo author sharedMsgId memId scope_ reaction add rcvMsg msgTs
|
||||
XMsgReact sharedMsgId memId scope_ reaction add -> void $ groupMsgReaction gInfo author_ sharedMsgId memId scope_ reaction add rcvMsg msgTs
|
||||
XFileCancel sharedMsgId -> void $ xFileCancelGroup gInfo author_ sharedMsgId
|
||||
XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p rcvMsg msgTs
|
||||
XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs
|
||||
@@ -3528,7 +3551,25 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do
|
||||
withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do
|
||||
let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body
|
||||
createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body Nothing
|
||||
forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed
|
||||
forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large"
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
where
|
||||
singleSenderGMId_ :: NonEmpty MessageDeliveryTask -> Maybe GroupMemberId
|
||||
singleSenderGMId_ (MessageDeliveryTask {senderGMId = senderGMId'} :| ts)
|
||||
| all (\MessageDeliveryTask {senderGMId} -> senderGMId == senderGMId') ts = Just senderGMId'
|
||||
| otherwise = Nothing
|
||||
DJReaction ->
|
||||
withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do
|
||||
let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks
|
||||
subscriberBody = encodeBinaryBatch
|
||||
[ encodeFwdElement (GrpMsgForward FwdChannel fwdBrokerTs) verifiedMsg
|
||||
| MessageDeliveryTask {taskId = tId, brokerTs = fwdBrokerTs, verifiedMsg} <- L.toList nextTasks
|
||||
, tId `elem` taskIds
|
||||
]
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body (Just subscriberBody)
|
||||
forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed
|
||||
forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large"
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
@@ -3545,7 +3586,7 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do
|
||||
fwd = GrpMsgForward {fwdSender, fwdBrokerTs}
|
||||
body = encodeBinaryBatch [encodeFwdElement fwd verifiedMsg]
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope (Just senderGMId) body
|
||||
createMsgDeliveryJob db gInfo jobScope (Just senderGMId) body Nothing
|
||||
updateDeliveryTaskStatus db (deliveryTaskId task) DTSProcessed
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
|
||||
@@ -3592,6 +3633,9 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
DJDeliveryJob _includePending -> do
|
||||
sendBodyToMembers
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
DJReaction -> do
|
||||
sendBodyToMembersReaction
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
DJRelayRemoved
|
||||
| workerScope /= DWSGroup ->
|
||||
throwChatError $ CEInternalError "delivery job worker: relay removed job in wrong worker scope"
|
||||
@@ -3600,7 +3644,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
deleteGroupConnections user gInfo True
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
where
|
||||
MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, cursorGMId_ = startingCursor} = job
|
||||
MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, subscriberBody_, cursorGMId_ = startingCursor} = job
|
||||
sendBodyToMembers :: CM ()
|
||||
sendBodyToMembers
|
||||
-- channel
|
||||
@@ -3662,26 +3706,44 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
memberRole >= GRModerator
|
||||
&& memberCurrent m
|
||||
&& maxVersion (memberChatVRange m) >= groupKnockingVersion
|
||||
sendBodyToMembersReaction :: CM ()
|
||||
sendBodyToMembersReaction
|
||||
| useRelays' gInfo = do
|
||||
bucketSize <- asks $ deliveryBucketSize . config
|
||||
sendLoopReaction bucketSize startingCursor
|
||||
| otherwise = -- fallback: deliver body to all (DJReaction should only be created for channels)
|
||||
deliver body =<< withStore' (\db -> getGroupMembersByCursor db vr user gInfo startingCursor singleSenderGMId_ 0)
|
||||
where
|
||||
deliver :: ByteString -> [GroupMember] -> CM ()
|
||||
deliver msgBody mems =
|
||||
let mConns = mapMaybe (fmap snd . readyMemberConn) mems
|
||||
msgReqs = foldMemConns mConns
|
||||
in void $ withAgent (`sendMessages` msgReqs)
|
||||
sendLoopReaction :: Int -> Maybe GroupMemberId -> CM ()
|
||||
sendLoopReaction bucketSize cursorGMId_ = do
|
||||
mems <- withStore' $ \db -> getGroupMembersByCursor db vr user gInfo cursorGMId_ singleSenderGMId_ bucketSize
|
||||
unless (null mems) $ do
|
||||
let (owners, nonOwners) = partition (\m -> memberRole' m >= GROwner) mems
|
||||
subBody = fromMaybe body subscriberBody_
|
||||
unless (null owners) $ deliver body owners
|
||||
unless (null nonOwners) $ deliver subBody nonOwners
|
||||
let cursorGMId' = groupMemberId' $ last mems
|
||||
withStore' $ \db -> updateDeliveryJobCursor db jobId cursorGMId'
|
||||
unless (length mems < bucketSize) $ sendLoopReaction bucketSize (Just cursorGMId')
|
||||
deliver :: ByteString -> [GroupMember] -> CM ()
|
||||
deliver msgBody mems =
|
||||
let mConns = mapMaybe (fmap snd . readyMemberConn) mems
|
||||
msgReqs = foldMemConns mConns
|
||||
in void $ withAgent (`sendMessages` msgReqs)
|
||||
where
|
||||
foldMemConns :: [Connection] -> [MsgReq]
|
||||
foldMemConns mConns = snd $ foldr' addReq (lastMemIdx_, []) mConns
|
||||
where
|
||||
foldMemConns :: [Connection] -> [MsgReq]
|
||||
foldMemConns mConns = snd $ foldr' addReq (lastMemIdx_, []) mConns
|
||||
lastMemIdx_ = let len = length mConns in if len > 1 then Just len else Nothing
|
||||
addReq :: Connection -> (Maybe Int, [MsgReq]) -> (Maybe Int, [MsgReq])
|
||||
addReq conn (memIdx_, reqs) =
|
||||
(subtract 1 <$> memIdx_, req : reqs)
|
||||
where
|
||||
lastMemIdx_ = let len = length mConns in if len > 1 then Just len else Nothing
|
||||
addReq :: Connection -> (Maybe Int, [MsgReq]) -> (Maybe Int, [MsgReq])
|
||||
addReq conn (memIdx_, reqs) =
|
||||
(subtract 1 <$> memIdx_, req : reqs)
|
||||
where
|
||||
req = (aConnId conn, PQEncOff, MsgFlags False, vrValue_)
|
||||
vrValue_ = case memIdx_ of
|
||||
Nothing -> VRValue Nothing msgBody -- sending to one member, do not reference body
|
||||
Just 1 -> VRValue (Just 1) msgBody
|
||||
Just _ -> VRRef 1
|
||||
req = (aConnId conn, PQEncOff, MsgFlags False, vrValue_)
|
||||
vrValue_ = case memIdx_ of
|
||||
Nothing -> VRValue Nothing msgBody -- sending to one member, do not reference body
|
||||
Just 1 -> VRValue (Just 1) msgBody
|
||||
Just _ -> VRRef 1
|
||||
|
||||
-- Single worker processes all relay requests (XGrpRelayInv).
|
||||
-- We use map with a single key 1 to fit into existing worker management framework.
|
||||
|
||||
@@ -62,12 +62,14 @@ jobScopeRow_ :: DeliveryJobScope -> DeliveryJobScopeRow
|
||||
jobScopeRow_ = \case
|
||||
DJSGroup {jobSpec} -> case jobSpec of
|
||||
DJDeliveryJob {includePending} -> (DWSGroup, Just DJSTDeliveryJob, Just (BI includePending), Nothing)
|
||||
DJReaction -> (DWSGroup, Just DJSTReaction, Nothing, Nothing)
|
||||
DJRelayRemoved -> (DWSGroup, Just DJSTRelayRemoved, Nothing, Nothing)
|
||||
DJSMemberSupport {supportGMId} -> (DWSMemberSupport, Nothing, Nothing, Just supportGMId)
|
||||
|
||||
toJobScope_ :: DeliveryJobScopeRow -> Maybe DeliveryJobScope
|
||||
toJobScope_ = \case
|
||||
(DWSGroup, Just DJSTDeliveryJob, Just (BI includePending), Nothing) -> Just $ DJSGroup {jobSpec = DJDeliveryJob {includePending}}
|
||||
(DWSGroup, Just DJSTReaction, Nothing, Nothing) -> Just $ DJSGroup {jobSpec = DJReaction}
|
||||
(DWSGroup, Just DJSTRelayRemoved, Nothing, Nothing) -> Just $ DJSGroup {jobSpec = DJRelayRemoved}
|
||||
(DWSMemberSupport, Nothing, Nothing, Just supportGMId) -> Just $ DJSMemberSupport {supportGMId}
|
||||
_ -> Nothing
|
||||
@@ -180,7 +182,7 @@ getNextDeliveryTasks db gInfo task =
|
||||
MessageDeliveryTask {jobScope, senderGMId} = task
|
||||
getTaskIds :: IO [Int64]
|
||||
getTaskIds
|
||||
| useRelays' gInfo =
|
||||
| useRelays' gInfo, not (isSenderFiltered jobScope) =
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
@@ -198,10 +200,10 @@ getNextDeliveryTasks db gInfo task =
|
||||
|]
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (Only DTSNew))
|
||||
| otherwise =
|
||||
-- For fully connected groups we guarantee a singleSenderGMId for a delivery job by additionally filtering
|
||||
-- on sender_group_member_id here, so that the job can then retrieve less members as recipients,
|
||||
-- optimizing for this single sender (see processDeliveryJob -> fully connected group branch).
|
||||
-- We do this optimization in the job to decrease load on admins using mobile devices for clients.
|
||||
-- Filtering on sender_group_member_id guarantees a singleSenderGMId for the delivery job.
|
||||
-- For fully connected groups this optimizes recipient retrieval (single sender exclusion).
|
||||
-- For DJReaction in relay groups this is required to ensure the sender is excluded from
|
||||
-- delivery, preventing subscribers from receiving their own reaction back via FwdChannel.
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
@@ -219,6 +221,9 @@ getNextDeliveryTasks db gInfo task =
|
||||
ORDER BY delivery_task_id ASC
|
||||
|]
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (senderGMId, DTSNew))
|
||||
isSenderFiltered :: DeliveryJobScope -> Bool
|
||||
isSenderFiltered (DJSGroup {jobSpec = DJReaction}) = True
|
||||
isSenderFiltered _ = False
|
||||
|
||||
updateDeliveryTaskStatus :: DB.Connection -> Int64 -> DeliveryTaskStatus -> IO ()
|
||||
updateDeliveryTaskStatus db taskId status = updateDeliveryTaskStatus_ db taskId status Nothing
|
||||
@@ -245,8 +250,8 @@ deleteDoneDeliveryTasks db createdAtCutoff = do
|
||||
|]
|
||||
(createdAtCutoff, DTSProcessed, DTSError)
|
||||
|
||||
createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> Maybe GroupMemberId -> ByteString -> IO ()
|
||||
createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body = do
|
||||
createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> Maybe GroupMemberId -> ByteString -> Maybe ByteString -> IO ()
|
||||
createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body subscriberBody_ = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
@@ -254,10 +259,10 @@ createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body = do
|
||||
INSERT INTO delivery_jobs (
|
||||
group_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
single_sender_group_member_id, body, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
single_sender_group_member_id, body, subscriber_body, job_status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (singleSenderGMId_, Binary body, DJSPending, currentTs, currentTs))
|
||||
((Only groupId) :. jobScopeRow_ jobScope :. (singleSenderGMId_, Binary body, Binary <$> subscriberBody_, DJSPending, currentTs, currentTs))
|
||||
where
|
||||
GroupInfo {groupId} = gInfo
|
||||
|
||||
@@ -272,7 +277,7 @@ getPendingDeliveryJobScopes db =
|
||||
|]
|
||||
(Only DJSPending)
|
||||
|
||||
type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe GroupMemberId, Binary ByteString, Maybe GroupMemberId)
|
||||
type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe GroupMemberId, Binary ByteString, Maybe (Binary ByteString), Maybe GroupMemberId)
|
||||
|
||||
getNextDeliveryJob :: DB.Connection -> DeliveryWorkerKey -> IO (Either StoreError (Maybe MessageDeliveryJob))
|
||||
getNextDeliveryJob db deliveryKey = do
|
||||
@@ -302,16 +307,18 @@ getNextDeliveryJob db deliveryKey = do
|
||||
SELECT
|
||||
delivery_job_id,
|
||||
worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id,
|
||||
single_sender_group_member_id, body, cursor_group_member_id
|
||||
single_sender_group_member_id, body, subscriber_body, cursor_group_member_id
|
||||
FROM delivery_jobs
|
||||
WHERE delivery_job_id = ?
|
||||
|]
|
||||
(Only jobId)
|
||||
where
|
||||
toDeliveryJob :: MessageDeliveryJobRow -> Either StoreError MessageDeliveryJob
|
||||
toDeliveryJob ((Only jobId') :. jobScopeRow :. (singleSenderGMId_, Binary body, cursorGMId_)) =
|
||||
toDeliveryJob ((Only jobId') :. jobScopeRow :. (singleSenderGMId_, Binary body, subscriberBodyBin_, cursorGMId_)) =
|
||||
case toJobScope_ jobScopeRow of
|
||||
Just jobScope -> Right $ MessageDeliveryJob {jobId = jobId', jobScope, singleSenderGMId_, body, cursorGMId_}
|
||||
Just jobScope ->
|
||||
let subscriberBody_ = (\(Binary bs) -> bs) <$> subscriberBodyBin_
|
||||
in Right $ MessageDeliveryJob {jobId = jobId', jobScope, singleSenderGMId_, body, subscriberBody_, cursorGMId_}
|
||||
Nothing -> Left $ SEInvalidDeliveryJob jobId'
|
||||
markJobFailed :: Int64 -> IO ()
|
||||
markJobFailed jobId =
|
||||
|
||||
@@ -94,6 +94,7 @@ module Simplex.Chat.Store.Messages
|
||||
getGroupCIReactions,
|
||||
getGroupReactions,
|
||||
setGroupReaction,
|
||||
setGroupReactionNoMember,
|
||||
getReactionMembers,
|
||||
getChatItemIdsByAgentMsgId,
|
||||
getDirectChatItem,
|
||||
@@ -3443,6 +3444,31 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti
|
||||
|]
|
||||
(groupId, groupMemberId' m, itemSharedMId, itemMemberId, BI sent, reaction)
|
||||
|
||||
setGroupReactionNoMember :: DB.Connection -> GroupInfo -> Maybe MemberId -> SharedMsgId -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO ()
|
||||
setGroupReactionNoMember db GroupInfo {groupId} itemMemberId itemSharedMId reaction add msgId reactionTs
|
||||
| add =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO chat_item_reactions
|
||||
(group_id, group_member_id, item_member_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts)
|
||||
VALUES (?,NULL,?,?,0,?,?,?)
|
||||
|]
|
||||
(groupId, itemMemberId, itemSharedMId, reaction, msgId, reactionTs)
|
||||
| otherwise =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
DELETE FROM chat_item_reactions
|
||||
WHERE chat_item_reaction_id = (
|
||||
SELECT chat_item_reaction_id FROM chat_item_reactions
|
||||
WHERE group_id = ? AND group_member_id IS NULL AND shared_msg_id = ?
|
||||
AND item_member_id IS NOT DISTINCT FROM ? AND reaction_sent = 0 AND reaction = ?
|
||||
LIMIT 1
|
||||
)
|
||||
|]
|
||||
(groupId, itemSharedMId, itemMemberId, reaction)
|
||||
|
||||
getReactionMembers :: DB.Connection -> VersionRangeChat -> User -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction]
|
||||
getReactionMembers db vr user groupId itemSharedMId reaction = do
|
||||
reactions <-
|
||||
|
||||
@@ -28,6 +28,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260430_subscriber_body
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -55,7 +56,8 @@ schemaMigrations =
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed)
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260430_subscriber_body", m20260430_subscriber_body, Just down_m20260430_subscriber_body)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260430_subscriber_body where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260430_subscriber_body :: Text
|
||||
m20260430_subscriber_body =
|
||||
[r|
|
||||
ALTER TABLE delivery_jobs ADD COLUMN subscriber_body BYTEA;
|
||||
|]
|
||||
|
||||
down_m20260430_subscriber_body :: Text
|
||||
down_m20260430_subscriber_body =
|
||||
[r|
|
||||
ALTER TABLE delivery_jobs DROP COLUMN subscriber_body;
|
||||
|]
|
||||
@@ -637,6 +637,7 @@ CREATE TABLE test_chat_schema.delivery_jobs (
|
||||
job_scope_support_gm_id bigint,
|
||||
single_sender_group_member_id bigint,
|
||||
body bytea,
|
||||
subscriber_body bytea,
|
||||
cursor_group_member_id bigint,
|
||||
job_status text NOT NULL,
|
||||
job_err_reason text,
|
||||
|
||||
@@ -151,6 +151,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260430_subscriber_body
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -301,7 +302,8 @@ schemaMigrations =
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed)
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260430_subscriber_body", m20260430_subscriber_body, Just down_m20260430_subscriber_body)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260430_subscriber_body where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260430_subscriber_body :: Query
|
||||
m20260430_subscriber_body =
|
||||
[sql|
|
||||
ALTER TABLE delivery_jobs ADD COLUMN subscriber_body BLOB;
|
||||
|]
|
||||
|
||||
down_m20260430_subscriber_body :: Query
|
||||
down_m20260430_subscriber_body =
|
||||
[sql|
|
||||
ALTER TABLE delivery_jobs DROP COLUMN subscriber_body;
|
||||
|]
|
||||
@@ -732,6 +732,7 @@ CREATE TABLE delivery_jobs(
|
||||
job_scope_support_gm_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE,
|
||||
single_sender_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE,
|
||||
body BLOB,
|
||||
subscriber_body BLOB,
|
||||
cursor_group_member_id INTEGER,
|
||||
job_status TEXT NOT NULL,
|
||||
job_err_reason TEXT,
|
||||
|
||||
Reference in New Issue
Block a user