Files
simplex-chat/src/Simplex/Chat/Delivery.hs
Evgeny 2db92ff6ed core: group member/owner keys for signing important messages (#6597)
* rfc: member keys

* update plan

* new encoding for message batches

* send new batch encoding in relay-based groups

* mvp launch plan

* update plan

* core: verify group member keys (#6669)

* core: verify group member keys

* refactor, process forwards

* refactor parsing

* refactor parsing 2

* refactor parser 3

* update rfc

* simplify

* simplify

* log tag

* refactor tag logging

* refactor withVerifiedSig

* simplify

* refactor more

* comment

* fix encoding

* fix sending as group for the new binary batch encoding

* unify types

* update api docs

* clean up

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* core: signing messages with member keys (#6675)

* core: signing messages with member keys (types)

* sign messages

* refactor batching

* better

* refactor

* remove unused Eq

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* core: forward signed messages as unchanged binary strings (#6678)

* core: forward signed messages as unchanged binary strings

* refactor

* consolidate types

* refactor VerifiedMsg

* refactor more

* undo rename

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* update schema and plans

* add signed status to chat items and events

* test signed chat items

* unify parser

* PostgreSQL fix, remove unused fields, option to send inline files in the tests

* change inline files config

* revert inline config change

* use different characters in batch encoding, to avoid conflict with inline files

* fix test, api docs, query plans

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-03-16 10:46:35 +00:00

197 lines
6.3 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Delivery where
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.Maybe (fromMaybe)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Messages (GroupChatScopeInfo (..), MessageId, ShowGroupAsSender)
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Encoding.String
type DeliveryWorkerKey = (GroupId, DeliveryWorkerScope)
data DeliveryWorkerScope
= DWSGroup
| DWSMemberSupport
-- | DWSMemberProfileUpdate
deriving (Eq, Ord, Show)
instance FromField DeliveryWorkerScope where fromField = fromTextField_ textDecode
instance ToField DeliveryWorkerScope where toField = toField . textEncode
instance TextEncoding DeliveryWorkerScope where
textDecode = \case
"group" -> Just DWSGroup
"member_support" -> Just DWSMemberSupport
-- "member_profile_update" -> Just DWSMemberProfileUpdate
_ -> Nothing
textEncode = \case
DWSGroup -> "group"
DWSMemberSupport -> "member_support"
-- DWSMemberProfileUpdate -> "member_profile_update"
-- Context for creating a delivery task. Separate from DeliveryJobScope because
-- sentAsGroup is only needed for task persistence and batching into XGrpMsgForward events.
-- Once batched into jobs, sentAsGroup=True and sentAsGroup=False messages can be mixed,
-- so jobs don't need this flag.
data DeliveryTaskContext = DeliveryTaskContext
{ jobScope :: DeliveryJobScope,
sentAsGroup :: ShowGroupAsSender
}
deriving (Show)
data DeliveryJobScope
= DJSGroup {jobSpec :: DeliveryJobSpec}
| DJSMemberSupport {supportGMId :: GroupMemberId}
-- | DJSMemberProfileUpdate
deriving (Show)
data DeliveryJobSpec
= DJDeliveryJob {includePending :: Bool}
| DJRelayRemoved
deriving (Show)
data DeliveryJobSpecTag
= DJSTDeliveryJob
| DJSTRelayRemoved
deriving (Show)
instance FromField DeliveryJobSpecTag where fromField = fromTextField_ textDecode
instance ToField DeliveryJobSpecTag where toField = toField . textEncode
instance TextEncoding DeliveryJobSpecTag where
textDecode = \case
"delivery_job" -> Just DJSTDeliveryJob
"relay_removed" -> Just DJSTRelayRemoved
_ -> Nothing
textEncode = \case
DJSTDeliveryJob -> "delivery_job"
DJSTRelayRemoved -> "relay_removed"
toWorkerScope :: DeliveryJobScope -> DeliveryWorkerScope
toWorkerScope = \case
DJSGroup _ -> DWSGroup
DJSMemberSupport _ -> DWSMemberSupport
-- DJSMemberProfileUpdate -> DWSMemberProfileUpdate
isRelayRemoved :: DeliveryJobScope -> Bool
isRelayRemoved = \case
DJSGroup {jobSpec} -> case jobSpec of
DJRelayRemoved -> True
_ -> False
_ -> False
jobScopeImpliedSpec :: DeliveryJobScope -> DeliveryJobSpec
jobScopeImpliedSpec = \case
DJSGroup {jobSpec} -> jobSpec
DJSMemberSupport {} -> DJDeliveryJob {includePending = False}
jobSpecImpliedPending :: DeliveryJobSpec -> Bool
jobSpecImpliedPending = \case
DJDeliveryJob {includePending} -> includePending
DJRelayRemoved -> True
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}
memberEventDeliveryScope :: GroupMember -> Maybe DeliveryJobScope
memberEventDeliveryScope m@GroupMember {memberRole, memberStatus}
| memberStatus == GSMemPendingApproval = Nothing
| memberStatus == GSMemPendingReview = Just $ DJSMemberSupport {supportGMId = groupMemberId' m}
| memberRole >= GRModerator = Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}}
| otherwise = Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
data NewMessageDeliveryTask = NewMessageDeliveryTask
{ messageId :: MessageId,
taskContext :: DeliveryTaskContext
}
deriving (Show)
data MessageDeliveryTask = MessageDeliveryTask
{ taskId :: Int64,
jobScope :: DeliveryJobScope,
senderGMId :: GroupMemberId,
fwdSender :: FwdSender,
brokerTs :: UTCTime,
verifiedMsg :: VerifiedMsg 'Json
}
deliveryTaskId :: MessageDeliveryTask -> Int64
deliveryTaskId = taskId
data DeliveryTaskStatus
= DTSNew -- created for delivery task worker to pick up and convert into a delivery job
| DTSProcessed -- processed by delivery task worker, delivery job created, task can be deleted
| DTSError -- permanent error
deriving (Show)
instance FromField DeliveryTaskStatus where fromField = fromTextField_ textDecode
instance ToField DeliveryTaskStatus where toField = toField . textEncode
instance TextEncoding DeliveryTaskStatus where
textDecode = \case
"new" -> Just DTSNew
"processed" -> Just DTSProcessed
"error" -> Just DTSError
_ -> Nothing
textEncode = \case
DTSNew -> "new"
DTSProcessed -> "processed"
DTSError -> "error"
data MessageDeliveryJob = MessageDeliveryJob
{ jobId :: Int64,
jobScope :: DeliveryJobScope,
singleSenderGMId_ :: Maybe GroupMemberId, -- Just for single-sender deliveries, Nothing for multi-sender deliveries
body :: ByteString,
cursorGMId_ :: Maybe GroupMemberId
}
deriving (Show)
deliveryJobId :: MessageDeliveryJob -> Int64
deliveryJobId = jobId
data DeliveryJobStatus
= DJSPending -- created for delivery job worker to pick up
| DJSComplete -- complete by delivery job worker, job can be deleted
| DJSError -- permanent error
deriving (Show)
instance FromField DeliveryJobStatus where fromField = fromTextField_ textDecode
instance ToField DeliveryJobStatus where toField = toField . textEncode
instance TextEncoding DeliveryJobStatus where
textDecode = \case
"pending" -> Just DJSPending
"complete" -> Just DJSComplete
"error" -> Just DJSError
_ -> Nothing
textEncode = \case
DJSPending -> "pending"
DJSComplete -> "complete"
DJSError -> "error"
-- data MemberProfileUpdateTask = undefined
-- data MemberProfileUpdateJob = undefined