mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 03:45:01 +00:00
core: forward based on relations vector (#6464)
* core: forward based on relations vector wip * fix introductions * fix forwarding tests * fix forwarding inside support scope * fix deduplication test * fix more tests * plans, api * live migration wip * enable tests * member locks * api * plans * fix for postgres * fix for postgres * rename predicate * rename predicate * optimize * refactor * fix * check * move part of migration to sql * plans * core: preserve detailed information in relation vectors (#6484) * core: relations vector live migrations; stage 2 migration sql (#6472) * rework forwarding in support scope * move operations inside transactions * set_member_vector_new_relation function * read vector ad-hoc * partition in transaction * fix postgres * postgres schema * api * plans * remove comment * lock before migration computation * refactor * simplify set relations * retreive only support scope members * fix * refactor * fix comment * enable tests * 1 second * for update * locks * fix mask * plans * fix * postgres --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
parent
1a22a34c41
commit
f76e994034
@@ -46,7 +46,7 @@ runDirectoryMigrations :: DirectoryOpts -> ChatConfig -> DBStore -> IO ()
|
||||
runDirectoryMigrations opts ChatConfig {confirmMigrations} chatStore =
|
||||
migrateDBSchema
|
||||
chatStore
|
||||
(toDBOpts dbOptions chatSuffix False)
|
||||
(toDBOpts dbOptions chatSuffix False [])
|
||||
(Just "sx_directory_migrations")
|
||||
directorySchemaMigrations
|
||||
MigrationConfig {confirm, backupPath = Nothing}
|
||||
|
||||
+11
-3
@@ -3349,6 +3349,14 @@ GroupMemberNotFound:
|
||||
- type: "groupMemberNotFound"
|
||||
- groupMemberId: int64
|
||||
|
||||
GroupMemberNotFoundByIndex:
|
||||
- type: "groupMemberNotFoundByIndex"
|
||||
- groupMemberIndex: int64
|
||||
|
||||
MemberRelationsVectorNotFound:
|
||||
- type: "memberRelationsVectorNotFound"
|
||||
- groupMemberId: int64
|
||||
|
||||
GroupHostMemberNotFound:
|
||||
- type: "groupHostMemberNotFound"
|
||||
- groupId: int64
|
||||
@@ -3361,6 +3369,9 @@ MemberContactGroupMemberNotFound:
|
||||
- type: "memberContactGroupMemberNotFound"
|
||||
- contactId: int64
|
||||
|
||||
InvalidMemberRelationUpdate:
|
||||
- type: "invalidMemberRelationUpdate"
|
||||
|
||||
GroupWithoutUser:
|
||||
- type: "groupWithoutUser"
|
||||
|
||||
@@ -3447,9 +3458,6 @@ PendingConnectionNotFound:
|
||||
- type: "pendingConnectionNotFound"
|
||||
- connId: int64
|
||||
|
||||
IntroNotFound:
|
||||
- type: "introNotFound"
|
||||
|
||||
UniqueID:
|
||||
- type: "uniqueID"
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 3016b929b48d3116f8ee60169c06383ca57f78e0
|
||||
tag: 92a9579e6958026e42a45e32c0e62fca243b4b0f
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -72,10 +72,11 @@ note over A, B: Vectors (only Dan/Cath relation interests us<br>- we want to avo
|
||||
note left of A: Alice vectors<br>For Cath: Dan - MRNew<br>For Dan: Cath - MRNew
|
||||
note right of B: Bob vectors<br>For Cath: Dan - MRIntroduced<br>For Dan: Cath - MRIntroducedTo
|
||||
note over A, B: Only Bob forwards between Cath and Dan
|
||||
C <<->> D: connect
|
||||
C ->> B: x.grp.mem.con (connected to Dan)
|
||||
D ->> B: or: x.grp.mem.con (connected to Cath)<br>(x.grp.mem.con from either is enough)
|
||||
note right of B: Bob vectors<br>For Cath: Dan - MRConnected<br>For Dan: Cath - MRConnected
|
||||
note over A, B: Only Bob forwards between Cath and Dan
|
||||
note over A, B: Bob stops forwarding between Cath and Dan
|
||||
```
|
||||
|
||||
### Avoid duplicate introductions
|
||||
|
||||
@@ -3735,9 +3735,12 @@ export type StoreError =
|
||||
| StoreError.GroupNotFoundByName
|
||||
| StoreError.GroupMemberNameNotFound
|
||||
| StoreError.GroupMemberNotFound
|
||||
| StoreError.GroupMemberNotFoundByIndex
|
||||
| StoreError.MemberRelationsVectorNotFound
|
||||
| StoreError.GroupHostMemberNotFound
|
||||
| StoreError.GroupMemberNotFoundByMemberId
|
||||
| StoreError.MemberContactGroupMemberNotFound
|
||||
| StoreError.InvalidMemberRelationUpdate
|
||||
| StoreError.GroupWithoutUser
|
||||
| StoreError.DuplicateGroupMember
|
||||
| StoreError.GroupAlreadyJoined
|
||||
@@ -3761,7 +3764,6 @@ export type StoreError =
|
||||
| StoreError.ConnectionNotFoundById
|
||||
| StoreError.ConnectionNotFoundByMemberId
|
||||
| StoreError.PendingConnectionNotFound
|
||||
| StoreError.IntroNotFound
|
||||
| StoreError.UniqueID
|
||||
| StoreError.LargeMsg
|
||||
| StoreError.InternalError
|
||||
@@ -3820,9 +3822,12 @@ export namespace StoreError {
|
||||
| "groupNotFoundByName"
|
||||
| "groupMemberNameNotFound"
|
||||
| "groupMemberNotFound"
|
||||
| "groupMemberNotFoundByIndex"
|
||||
| "memberRelationsVectorNotFound"
|
||||
| "groupHostMemberNotFound"
|
||||
| "groupMemberNotFoundByMemberId"
|
||||
| "memberContactGroupMemberNotFound"
|
||||
| "invalidMemberRelationUpdate"
|
||||
| "groupWithoutUser"
|
||||
| "duplicateGroupMember"
|
||||
| "groupAlreadyJoined"
|
||||
@@ -3846,7 +3851,6 @@ export namespace StoreError {
|
||||
| "connectionNotFoundById"
|
||||
| "connectionNotFoundByMemberId"
|
||||
| "pendingConnectionNotFound"
|
||||
| "introNotFound"
|
||||
| "uniqueID"
|
||||
| "largeMsg"
|
||||
| "internalError"
|
||||
@@ -3988,6 +3992,16 @@ export namespace StoreError {
|
||||
groupMemberId: number // int64
|
||||
}
|
||||
|
||||
export interface GroupMemberNotFoundByIndex extends Interface {
|
||||
type: "groupMemberNotFoundByIndex"
|
||||
groupMemberIndex: number // int64
|
||||
}
|
||||
|
||||
export interface MemberRelationsVectorNotFound extends Interface {
|
||||
type: "memberRelationsVectorNotFound"
|
||||
groupMemberId: number // int64
|
||||
}
|
||||
|
||||
export interface GroupHostMemberNotFound extends Interface {
|
||||
type: "groupHostMemberNotFound"
|
||||
groupId: number // int64
|
||||
@@ -4003,6 +4017,10 @@ export namespace StoreError {
|
||||
contactId: number // int64
|
||||
}
|
||||
|
||||
export interface InvalidMemberRelationUpdate extends Interface {
|
||||
type: "invalidMemberRelationUpdate"
|
||||
}
|
||||
|
||||
export interface GroupWithoutUser extends Interface {
|
||||
type: "groupWithoutUser"
|
||||
}
|
||||
@@ -4112,10 +4130,6 @@ export namespace StoreError {
|
||||
connId: number // int64
|
||||
}
|
||||
|
||||
export interface IntroNotFound extends Interface {
|
||||
type: "introNotFound"
|
||||
}
|
||||
|
||||
export interface UniqueID extends Interface {
|
||||
type: "uniqueID"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."3016b929b48d3116f8ee60169c06383ca57f78e0" = "1gvv96w1ag8xh19an5j50isjw0yq1cbsywii5s1adaplr6fw34mb";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."92a9579e6958026e42a45e32c0e62fca243b4b0f" = "0hacyi4hsbca55l4n2n4pcsmsfvh2jaw681j6106y5mkillmb3fv";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
@@ -123,6 +123,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -269,6 +270,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
+2
-2
@@ -118,8 +118,8 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
createChatDatabase :: ChatDbOpts -> MigrationConfig -> IO (Either MigrationError ChatDatabase)
|
||||
createChatDatabase chatDbOpts migrationConfig = runExceptT $ do
|
||||
chatStore <- ExceptT $ createChatStore (toDBOpts chatDbOpts chatSuffix False) migrationConfig
|
||||
agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False) migrationConfig
|
||||
chatStore <- ExceptT $ createChatStore (toDBOpts chatDbOpts chatSuffix False chatDBFunctions) migrationConfig
|
||||
agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False []) migrationConfig
|
||||
pure ChatDatabase {chatStore, agentStore}
|
||||
|
||||
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController
|
||||
|
||||
@@ -167,6 +167,9 @@ startChatController mainApp enableSndFiles = do
|
||||
runExceptT (syncConnections' users) >>= \case
|
||||
Left e -> liftIO $ putStrLn $ "Error synchronizing connections: " <> show e
|
||||
Right _ -> pure ()
|
||||
runExceptT migrateMemberRelations >>= \case
|
||||
Left e -> liftIO $ putStrLn $ "Error migrating member relations: " <> show e
|
||||
Right _ -> pure ()
|
||||
restoreCalls
|
||||
s <- asks agentAsync
|
||||
readTVarIO s >>= maybe (start s users) (pure . fst)
|
||||
@@ -178,6 +181,10 @@ startChatController mainApp enableSndFiles = do
|
||||
(userDiff, connDiff) <- withAgent (\a -> syncConnections a aUserIds connIds)
|
||||
withFastStore' setConnectionsSyncTs
|
||||
toView $ CEvtConnectionsDiff (AgentUserId <$> userDiff) (AgentConnId <$> connDiff)
|
||||
migrateMemberRelations =
|
||||
when mainApp $
|
||||
whenM (withStore' hasMembersWithoutVector) $
|
||||
void $ forkIO runRelationsVectorMigration
|
||||
start s users = do
|
||||
a1 <- async agentSubscriber
|
||||
a2 <-
|
||||
@@ -4163,6 +4170,21 @@ agentSubscriber = do
|
||||
|
||||
type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId))
|
||||
|
||||
runRelationsVectorMigration :: CM ()
|
||||
runRelationsVectorMigration = do
|
||||
liftIO $ threadDelay' 5000000 -- 5 seconds (initial delay)
|
||||
migrateMembers
|
||||
where
|
||||
stepDelay = 1000000 -- 1 second
|
||||
migrateMembers = flip catchAllErrors eToView $ do
|
||||
lift waitChatStartedAndActivated
|
||||
gmIds <- withStore' getGMsWithoutVectorIds
|
||||
forM_ gmIds $ \gmId -> do
|
||||
lift waitChatStartedAndActivated
|
||||
withStore' (`migrateMemberRelationsVector'` gmId) `catchAllErrors` eToView
|
||||
liftIO $ threadDelay' stepDelay
|
||||
unless (null gmIds) migrateMembers
|
||||
|
||||
cleanupManager :: CM ()
|
||||
cleanupManager = do
|
||||
interval <- asks (cleanupManagerInterval . config)
|
||||
|
||||
@@ -73,6 +73,7 @@ import Simplex.Chat.Store.Messages
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Util (encryptFile, shuffle)
|
||||
@@ -1024,65 +1025,84 @@ introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember ->
|
||||
introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do
|
||||
forM_ (memberConn m) $ \mConn -> do
|
||||
let msg =
|
||||
if (maxVersion (memberChatVRange m) >= groupKnockingVersion)
|
||||
if maxVersion (memberChatVRange m) >= groupKnockingVersion
|
||||
then XGrpLinkAcpt GAPendingReview memberRole memberId
|
||||
else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing
|
||||
void $ sendDirectMemberMessage mConn msg groupId
|
||||
modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo
|
||||
let rcpModMs = filter (\mem -> memberCurrent mem && maxVersion (memberChatVRange mem) >= groupKnockingVersion) modMs
|
||||
let rcpModMs = filter shouldIntroduce modMs
|
||||
introduceMember vr user gInfo m rcpModMs (Just $ MSMember $ memberId' m)
|
||||
where
|
||||
shouldIntroduce :: GroupMember -> Bool
|
||||
shouldIntroduce mem =
|
||||
memberCurrent mem
|
||||
&& groupMemberId' mem /= groupMemberId' m
|
||||
&& maxVersion (memberChatVRange mem) >= groupKnockingVersion
|
||||
|
||||
introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToAll vr user gInfo m = do
|
||||
members <- withStore' $ \db -> getGroupMembersForIntroduction db vr user gInfo m
|
||||
let recipients = filter memberCurrent members
|
||||
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
|
||||
vector_ <- withStore' (`getMemberRelationsVector_` m)
|
||||
let recipients = filter (shouldIntroduce vector_) members
|
||||
introduceMember vr user gInfo m recipients Nothing
|
||||
where
|
||||
shouldIntroduce :: Maybe ByteString -> GroupMember -> Bool
|
||||
shouldIntroduce vector_ m' =
|
||||
memberCurrent m'
|
||||
&& groupMemberId' m' /= groupMemberId' m
|
||||
&& maybe True (\v -> getRelation (indexInGroup m') v == MRNew) vector_
|
||||
|
||||
introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToRemaining vr user gInfo m = do
|
||||
(members, introducedGMIds) <-
|
||||
withStore' $ \db -> (,) <$> getGroupMembersForIntroduction db vr user gInfo m <*> getIntroducedGroupMemberIds db m
|
||||
let recipients = filter (introduceMemP introducedGMIds) members
|
||||
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
|
||||
vector_ <- withStore' (`getMemberRelationsVector_` m)
|
||||
recipients <- filterRecipients vector_ members
|
||||
introduceMember vr user gInfo m recipients Nothing
|
||||
where
|
||||
introduceMemP introducedGMIds mem =
|
||||
memberCurrent mem
|
||||
&& groupMemberId' mem `notElem` introducedGMIds
|
||||
&& groupMemberId' mem /= groupMemberId' m
|
||||
filterRecipients :: Maybe ByteString -> [GroupMember] -> CM [GroupMember]
|
||||
filterRecipients vector_ members = do
|
||||
newRelation <- case vector_ of
|
||||
Nothing -> do
|
||||
introducedGMIds <- S.fromList <$> withStore' (`getIntroducedGroupMemberIds` m)
|
||||
pure $ \m' -> groupMemberId' m' `S.notMember` introducedGMIds
|
||||
Just vec -> pure $ \m' -> getRelation (indexInGroup m') vec == MRNew
|
||||
pure $ filter (\m' -> groupMemberId' m' /= groupMemberId' m && memberCurrent m' && newRelation m') members
|
||||
|
||||
introduceMember :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
|
||||
introduceMember _ _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active"
|
||||
introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
|
||||
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo m) msgScope
|
||||
introduceMember vr user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
|
||||
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo toMember) msgScope
|
||||
sendIntroductions introduceToMembers
|
||||
where
|
||||
sendIntroductions members = do
|
||||
intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m
|
||||
shuffledIntros <- liftIO $ shuffleIntros intros
|
||||
if m `supportsVersion` batchSendVersion
|
||||
sendIntroductions reMembers = do
|
||||
updateToMemberVector reMembers
|
||||
reMembers' <- withStore' $ \db -> createIntrosOrUpdateVectors db vr reMembers toMember
|
||||
shuffledReMembers <- liftIO $ shuffleMembers reMembers'
|
||||
if toMember `supportsVersion` batchSendVersion
|
||||
then do
|
||||
let events = map (memberIntro . reMember) shuffledIntros
|
||||
let events = map memberIntro shuffledReMembers
|
||||
forM_ (L.nonEmpty events) $ \events' ->
|
||||
sendGroupMemberMessages user conn events' groupId
|
||||
else forM_ shuffledIntros $ \intro ->
|
||||
processIntro intro `catchAllErrors` eToView
|
||||
else forM_ shuffledReMembers $ \reMember ->
|
||||
void $ sendDirectMemberMessage conn (memberIntro reMember) groupId
|
||||
updateToMemberVector :: [GroupMember] -> CM ()
|
||||
updateToMemberVector reMembers = do
|
||||
let relations = map (\GroupMember {indexInGroup} -> (indexInGroup, (IDReferencedIntroduced, MRIntroduced))) reMembers
|
||||
withStore' $ \db -> setMemberVectorNewRelations db toMember relations
|
||||
memberIntro :: GroupMember -> ChatMsgEvent 'Json
|
||||
memberIntro reMember =
|
||||
let mInfo = memberInfo gInfo reMember
|
||||
mRestrictions = memberRestrictions reMember
|
||||
in XGrpMemIntro mInfo mRestrictions
|
||||
shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro]
|
||||
shuffleIntros intros = do
|
||||
let (admins, others) = partition isAdmin intros
|
||||
shuffleMembers :: [GroupMember] -> IO [GroupMember]
|
||||
shuffleMembers reMembers = do
|
||||
let (admins, others) = partition isAdmin reMembers
|
||||
(admPics, admNoPics) = partition hasPicture admins
|
||||
(othPics, othNoPics) = partition hasPicture others
|
||||
mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics]
|
||||
where
|
||||
isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin
|
||||
hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image
|
||||
processIntro intro@GroupMemberIntro {introId} = do
|
||||
void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId
|
||||
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
|
||||
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
|
||||
hasPicture GroupMember {memberProfile = LocalProfile {image}} = isJust image
|
||||
|
||||
userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile
|
||||
userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks
|
||||
@@ -2047,8 +2067,8 @@ readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {c
|
||||
| otherwise = Nothing
|
||||
readyMemberConn GroupMember {activeConn = Nothing} = Nothing
|
||||
|
||||
sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM ()
|
||||
sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do
|
||||
sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe GroupMemberIntro -> CM () -> CM ()
|
||||
sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent intro_ postDeliver = do
|
||||
msg <- createSndMessage chatMsgEvent (GroupId groupId)
|
||||
messageMember msg `catchAllErrors` eToView
|
||||
where
|
||||
@@ -2056,7 +2076,7 @@ sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} c
|
||||
messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case
|
||||
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
|
||||
MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
|
||||
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId (introId <$> intro_)
|
||||
MSAForwarded -> pure ()
|
||||
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
|
||||
@@ -28,7 +28,7 @@ import Data.Either (lefts, partitionEithers, rights)
|
||||
import Data.Foldable (foldr')
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find, foldl')
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
@@ -62,6 +62,7 @@ import Simplex.Chat.Store.Messages
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.FileTransfer.Description (ValidFileDescription)
|
||||
@@ -2615,13 +2616,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case
|
||||
Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist"
|
||||
Right reMember -> do
|
||||
introId <- withStore $ \db -> do
|
||||
GroupMemberIntro {introId} <- getIntroduction db reMember m
|
||||
liftIO $ updateIntroStatus db introId GMIntroInvReceived
|
||||
pure introId
|
||||
sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo gInfo m) introInv) (Just introId) $
|
||||
withStore' $
|
||||
\db -> updateIntroStatus db introId GMIntroInvForwarded
|
||||
intro_ <- withStore' $ \db -> getIntroduction db reMember m
|
||||
update intro_ GMIntroInvReceived
|
||||
sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo gInfo m) introInv) intro_ $
|
||||
update intro_ GMIntroInvForwarded
|
||||
where
|
||||
update (Just GroupMemberIntro {introId}) status = withStore' $ \db -> updateIntroStatus db introId status
|
||||
update Nothing _ = pure ()
|
||||
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
|
||||
|
||||
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> CM ()
|
||||
@@ -2715,45 +2716,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
blocked = mrsBlocked restriction
|
||||
|
||||
xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM ()
|
||||
xGrpMemCon gInfo sendingMember memId = do
|
||||
refMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId
|
||||
case (memberCategory sendingMember, memberCategory refMember) of
|
||||
(GCInviteeMember, GCInviteeMember) ->
|
||||
withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case
|
||||
Right intro -> inviteeXGrpMemCon intro
|
||||
Left _ ->
|
||||
withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case
|
||||
Right intro -> forwardMemberXGrpMemCon intro
|
||||
Left _ -> messageWarning "x.grp.mem.con: no introduction"
|
||||
(GCInviteeMember, _) ->
|
||||
withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case
|
||||
Right intro -> inviteeXGrpMemCon intro
|
||||
Left _ -> messageWarning "x.grp.mem.con: no introduction"
|
||||
(_, GCInviteeMember) ->
|
||||
withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case
|
||||
Right intro -> forwardMemberXGrpMemCon intro
|
||||
Left _ -> messageWarning "x.grp.mem.con: no introductiosupportn"
|
||||
-- Note: we can allow XGrpMemCon to all member categories if we decide to support broader group forwarding,
|
||||
-- deduplication (see saveGroupRcvMsg, saveGroupFwdRcvMsg) already supports sending XGrpMemCon
|
||||
-- to any forwarding member, not only host/inviting member;
|
||||
-- database would track all members connections then
|
||||
-- (currently it's done via group_member_intros for introduced connections only)
|
||||
_ ->
|
||||
messageWarning "x.grp.mem.con: neither member is invitee"
|
||||
where
|
||||
inviteeXGrpMemCon :: GroupMemberIntro -> CM ()
|
||||
inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of
|
||||
GMIntroReConnected -> updateStatus introId GMIntroConnected
|
||||
GMIntroToConnected -> pure ()
|
||||
GMIntroConnected -> pure ()
|
||||
_ -> updateStatus introId GMIntroToConnected
|
||||
forwardMemberXGrpMemCon :: GroupMemberIntro -> CM ()
|
||||
forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of
|
||||
GMIntroToConnected -> updateStatus introId GMIntroConnected
|
||||
GMIntroReConnected -> pure ()
|
||||
GMIntroConnected -> pure ()
|
||||
_ -> updateStatus introId GMIntroReConnected
|
||||
updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status
|
||||
xGrpMemCon gInfo sendingMem memId = do
|
||||
refMem <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId
|
||||
withStore' (`migrateMemberRelationsVector` sendingMem)
|
||||
withStore' (`migrateMemberRelationsVector` refMem)
|
||||
-- Updating vectors in separate transactions to avoid deadlocks.
|
||||
withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected
|
||||
withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected
|
||||
|
||||
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> ChatMessage 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope)
|
||||
xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages chatMsg msg brokerTs forwarded = do
|
||||
@@ -3238,10 +3207,10 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
DJSMemberSupport scopeGMId -> do
|
||||
-- for member support scope we just load all recipients in one go, without cursor
|
||||
modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo
|
||||
let moderatorFilter mem =
|
||||
memberCurrent mem
|
||||
&& maxVersion (memberChatVRange mem) >= groupKnockingVersion
|
||||
&& Just (groupMemberId' mem) /= singleSenderGMId_
|
||||
let moderatorFilter m =
|
||||
memberCurrent m
|
||||
&& maxVersion (memberChatVRange m) >= groupKnockingVersion
|
||||
&& Just (groupMemberId' m) /= singleSenderGMId_
|
||||
modMs' = filter moderatorFilter modMs
|
||||
mems <-
|
||||
if Just scopeGMId == singleSenderGMId_
|
||||
@@ -3255,42 +3224,30 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays"
|
||||
Just singleSenderGMId -> do
|
||||
sender <- withStore $ \db -> getGroupMemberById db vr user singleSenderGMId
|
||||
mems <- buildMemberList sender
|
||||
unless (null mems) $ deliver body mems
|
||||
ms <- buildMemberList sender
|
||||
unless (null ms) $ deliver body ms
|
||||
where
|
||||
buildMemberList sender = case jobScope of
|
||||
DJSGroup {jobSpec}
|
||||
| jobSpecImpliedPending jobSpec ->
|
||||
filter memberCurrentOrPending <$> getAllIntroducedAndInvited
|
||||
| otherwise ->
|
||||
filter memberCurrent <$> getAllIntroducedAndInvited
|
||||
DJSMemberSupport scopeGMId -> do
|
||||
-- moderators introduced to this invited member
|
||||
introducedModMs <-
|
||||
if memberCategory sender == GCInviteeMember
|
||||
then withStore' $ \db -> getForwardIntroducedModerators db vr user sender
|
||||
else pure []
|
||||
-- invited moderators to which this member was introduced
|
||||
invitedModMs <- withStore' $ \db -> getForwardInvitedModerators db vr user sender
|
||||
let modMs = introducedModMs <> invitedModMs
|
||||
modMs' = filter (\mem -> memberCurrent mem && maxVersion (memberChatVRange mem) >= groupKnockingVersion) modMs
|
||||
if scopeGMId == groupMemberId' sender
|
||||
then pure modMs'
|
||||
else
|
||||
withStore' (\db -> getForwardScopeMember db vr user sender scopeGMId) >>= \case
|
||||
Just scopeMem -> pure $ scopeMem : modMs'
|
||||
_ -> pure modMs'
|
||||
where
|
||||
getAllIntroducedAndInvited = do
|
||||
ChatConfig {highlyAvailable} <- asks config
|
||||
-- members introduced to this invited member
|
||||
introducedMembers <-
|
||||
if memberCategory sender == GCInviteeMember
|
||||
then withStore' $ \db -> getForwardIntroducedMembers db vr user sender highlyAvailable
|
||||
else pure []
|
||||
-- invited members to which this member was introduced
|
||||
invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user sender highlyAvailable
|
||||
pure $ introducedMembers <> invitedMembers
|
||||
buildMemberList sender = do
|
||||
vec <- withStore $ \db -> migrateGetMemberRelationsVector db sender
|
||||
-- this excludes the sender
|
||||
let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec
|
||||
case jobScope of
|
||||
DJSGroup {jobSpec} -> do
|
||||
ms <- withStore' $ \db -> getGroupMembersByIndexes db vr user gInfo introducedMemsIdxs
|
||||
pure $ filter shouldForwardTo ms
|
||||
where
|
||||
shouldForwardTo m
|
||||
| jobSpecImpliedPending jobSpec = memberCurrentOrPending m
|
||||
| otherwise = memberCurrent m
|
||||
DJSMemberSupport scopeGMId -> do
|
||||
ms <- withStore' $ \db -> getSupportScopeMembersByIndexes db vr user gInfo scopeGMId introducedMemsIdxs
|
||||
pure $ filter shouldForwardTo ms
|
||||
where
|
||||
shouldForwardTo m = groupMemberId' m == scopeGMId || currentModerator m
|
||||
currentModerator m@GroupMember {memberRole} =
|
||||
memberRole >= GRModerator
|
||||
&& memberCurrent m
|
||||
&& maxVersion (memberChatVRange m) >= groupKnockingVersion
|
||||
where
|
||||
deliver :: ByteString -> [GroupMember] -> CM ()
|
||||
deliver msgBody mems =
|
||||
|
||||
@@ -295,8 +295,8 @@ chatMigrateInitKey :: ChatDbOpts -> Bool -> String -> Bool -> IO (Either DBMigra
|
||||
chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do
|
||||
confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm
|
||||
let migrationConfig = MigrationConfig confirmMigrations (Just "")
|
||||
chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey) migrationConfig
|
||||
agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey) migrationConfig
|
||||
chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey chatDBFunctions) migrationConfig
|
||||
agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey []) migrationConfig
|
||||
liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore}
|
||||
where
|
||||
opts = mobileChatOpts $ removeDbKey chatDbOpts
|
||||
|
||||
@@ -58,8 +58,8 @@ migrationBackupPathP = pure Nothing
|
||||
dbString :: ChatDbOpts -> String
|
||||
dbString ChatDbOpts {dbConnstr} = dbConnstr
|
||||
|
||||
toDBOpts :: ChatDbOpts -> String -> Bool -> DBOpts
|
||||
toDBOpts ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} dbSuffix _keepKey =
|
||||
toDBOpts :: ChatDbOpts -> String -> Bool -> [()] -> DBOpts
|
||||
toDBOpts ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} dbSuffix _keepKey _dbFunctions =
|
||||
DBOpts
|
||||
{ connstr = B.pack dbConnstr,
|
||||
schema = B.pack $ if null dbSchemaPrefix then "simplex_v1" <> dbSuffix else dbSchemaPrefix <> dbSuffix,
|
||||
@@ -73,6 +73,9 @@ chatSuffix = "_chat_schema"
|
||||
agentSuffix :: String
|
||||
agentSuffix = "_agent_schema"
|
||||
|
||||
chatDBFunctions :: [()]
|
||||
chatDBFunctions = []
|
||||
|
||||
mobileDbOpts :: CString -> CString -> IO ChatDbOpts
|
||||
mobileDbOpts schemaPrefix connstr = do
|
||||
dbSchemaPrefix <- peekCString schemaPrefix
|
||||
|
||||
@@ -11,7 +11,9 @@ import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Foreign.C.String
|
||||
import Options.Applicative
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
|
||||
import Simplex.Messaging.Agent.Store.Interface (DBOpts (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common (SQLiteFuncDef (..), SQLiteFuncPtrs (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (TrackQueries (..))
|
||||
import System.FilePath (combine)
|
||||
|
||||
@@ -70,10 +72,11 @@ migrationBackupPathP =
|
||||
dbString :: ChatDbOpts -> String
|
||||
dbString ChatDbOpts {dbFilePrefix} = dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
|
||||
|
||||
toDBOpts :: ChatDbOpts -> String -> Bool -> DBOpts
|
||||
toDBOpts ChatDbOpts {dbFilePrefix, dbKey, trackQueries, vacuumOnMigration} dbSuffix keepKey = do
|
||||
toDBOpts :: ChatDbOpts -> String -> Bool -> [SQLiteFuncDef] -> DBOpts
|
||||
toDBOpts ChatDbOpts {dbFilePrefix, dbKey, trackQueries, vacuumOnMigration} dbSuffix keepKey dbFunctions = do
|
||||
DBOpts
|
||||
{ dbFilePath = dbFilePrefix <> dbSuffix,
|
||||
dbFunctions,
|
||||
dbKey,
|
||||
keepKey,
|
||||
vacuum = vacuumOnMigration,
|
||||
@@ -86,6 +89,12 @@ chatSuffix = "_chat.db"
|
||||
agentSuffix :: String
|
||||
agentSuffix = "_agent.db"
|
||||
|
||||
chatDBFunctions :: [SQLiteFuncDef]
|
||||
chatDBFunctions =
|
||||
[ SQLiteFuncDef "migrate_relations_vector" 3 (SQLiteAggrPtrs sqliteMemberRelationsStepPtr sqliteMemberRelationsFinalPtr),
|
||||
SQLiteFuncDef "set_member_vector_new_relation" 4 (SQLiteFuncPtr True sqliteSetMemberVectorNewRelationPtr)
|
||||
]
|
||||
|
||||
mobileDbOpts :: CString -> CString -> IO ChatDbOpts
|
||||
mobileDbOpts fp key = do
|
||||
dbFilePrefix <- peekCString fp
|
||||
|
||||
@@ -185,7 +185,7 @@ getNextDeliveryTasks db gInfo task =
|
||||
| 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 -> getForwardIntroducedMembers, etc.).
|
||||
-- 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.
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
|
||||
+300
-166
@@ -58,11 +58,13 @@ module Simplex.Chat.Store.Groups
|
||||
getMentionedGroupMember,
|
||||
getMentionedMemberByMemberId,
|
||||
getGroupMemberById,
|
||||
getGroupMemberByIndex,
|
||||
getGroupMemberByMemberId,
|
||||
getGroupMemberIdViaMemberId,
|
||||
getScopeMemberIdViaMemberId,
|
||||
getGroupMembers,
|
||||
getGroupMembersForIntroduction,
|
||||
getGroupMembersByIndexes,
|
||||
getSupportScopeMembersByIndexes,
|
||||
getGroupModerators,
|
||||
getGroupRelays,
|
||||
getGroupMembersForExpiration,
|
||||
@@ -98,14 +100,17 @@ module Simplex.Chat.Store.Groups
|
||||
deleteGroupMemberConnection,
|
||||
updateGroupMemberRole,
|
||||
createIntroductions,
|
||||
createIntrosOrUpdateVectors,
|
||||
setMemberVectorNewRelations,
|
||||
setMembersVectorsNewRelation,
|
||||
setMemberVectorRelationConnected,
|
||||
migrateGetMemberRelationsVector,
|
||||
migrateMemberRelationsVector,
|
||||
migrateMemberRelationsVector',
|
||||
getMemberRelationsVector_,
|
||||
updateIntroStatus,
|
||||
getIntroduction,
|
||||
getIntroducedGroupMemberIds,
|
||||
getForwardIntroducedMembers,
|
||||
getForwardIntroducedModerators,
|
||||
getForwardInvitedMembers,
|
||||
getForwardInvitedModerators,
|
||||
getForwardScopeMember,
|
||||
createIntroReMember,
|
||||
createIntroToMemberContact,
|
||||
getMatchingContacts,
|
||||
@@ -146,6 +151,8 @@ module Simplex.Chat.Store.Groups
|
||||
setGroupChatTTL,
|
||||
getGroupChatTTL,
|
||||
getUserGroupsToExpire,
|
||||
hasMembersWithoutVector,
|
||||
getGMsWithoutVectorIds,
|
||||
updateGroupAlias,
|
||||
)
|
||||
where
|
||||
@@ -155,8 +162,11 @@ import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (second)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Char (toLower)
|
||||
import Data.Either (rights)
|
||||
import Data.Foldable (foldrM)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (partition, sortOn)
|
||||
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
|
||||
@@ -170,6 +180,7 @@ import Simplex.Chat.Protocol hiding (Binary)
|
||||
import Simplex.Chat.Store.Direct
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRelation (..), setNewRelations, setRelationConnected, toIntroDirInt, toRelationInt)
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
@@ -180,11 +191,12 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff)
|
||||
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>), ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Version
|
||||
import UnliftIO.STM
|
||||
#if defined(dbPostgres)
|
||||
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
|
||||
import qualified Data.Set as S
|
||||
import Database.PostgreSQL.Simple (In (..), Only (..), Query, (:.) (..))
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
#else
|
||||
import Database.SQLite.Simple (Only (..), Query, (:.) (..))
|
||||
@@ -511,12 +523,12 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId)
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId)
|
||||
:. (userId, localDisplayName' userOrContact, contactId' userOrContact, localProfileId $ profile' userOrContact, createdAt, createdAt)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -530,12 +542,12 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId)
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId)
|
||||
:. (userId, incognitoLdn, contactId' userOrContact, localProfileId $ profile' userOrContact, customUserProfileId, createdAt, createdAt)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -570,11 +582,11 @@ createPreparedGroup db vr user@User {userId, userContactId} groupProfile busines
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, GRAdmin, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||
( (groupId, indexInGroup, memberId, GRAdmin, GCHostMember, GSMemAccepted, Binary B.empty, fromInvitedBy userContactId IBUnknown)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
)
|
||||
insertedRowId db
|
||||
@@ -766,11 +778,11 @@ createGroupViaLink'
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted, Binary B.empty, fromInvitedBy userContactId IBUnknown)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
)
|
||||
insertedRowId db
|
||||
@@ -994,6 +1006,22 @@ getGroupMemberById db vr user@User {userId} groupMemberId =
|
||||
(groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?")
|
||||
(groupMemberId, userId)
|
||||
|
||||
getGroupMemberByIndex :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupMember
|
||||
getGroupMemberByIndex db vr user GroupInfo {groupId} indexInGroup =
|
||||
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByIndex indexInGroup) $
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group = ?")
|
||||
(groupId, indexInGroup)
|
||||
|
||||
getSupportScopeMemberByIndex :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMemberId -> Int64 -> ExceptT StoreError IO GroupMember
|
||||
getSupportScopeMemberByIndex db vr user GroupInfo {groupId} scopeGMId indexInGroup =
|
||||
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByIndex indexInGroup) $
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group = ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?)")
|
||||
(groupId, indexInGroup, GRModerator, GRAdmin, GROwner, scopeGMId)
|
||||
|
||||
getGroupMemberByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember
|
||||
getGroupMemberByMemberId db vr user GroupInfo {groupId} memberId =
|
||||
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByMemberId memberId) $
|
||||
@@ -1017,20 +1045,38 @@ getGroupMemberIdViaMemberId db User {userId} GroupInfo {groupId} memberId =
|
||||
(userId, groupId, memberId)
|
||||
|
||||
getGroupMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} =
|
||||
map (toContactMember vr user)
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
|
||||
(userId, groupId, userContactId)
|
||||
|
||||
getGroupMembersForIntroduction :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> IO [GroupMember]
|
||||
getGroupMembersForIntroduction db vr user@User {userId, userContactId} GroupInfo {groupId} _introduced@GroupMember {indexInGroup} = do
|
||||
map (toContactMember vr user)
|
||||
<$> DB.query
|
||||
getGroupMembersByIndexes :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> [Int64] -> IO [GroupMember]
|
||||
getGroupMembersByIndexes db vr user gInfo indexesInGroup = do
|
||||
#if defined(dbPostgres)
|
||||
let GroupInfo {groupId} = gInfo
|
||||
map (toContactMember vr user) <$>
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND index_in_group < ?")
|
||||
(userId, groupId, userContactId, indexInGroup)
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group IN ?")
|
||||
(groupId, In indexesInGroup)
|
||||
#else
|
||||
rights <$> mapM (runExceptT . getGroupMemberByIndex db vr user gInfo) indexesInGroup
|
||||
#endif
|
||||
|
||||
getSupportScopeMembersByIndexes :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMemberId -> [Int64] -> IO [GroupMember]
|
||||
getSupportScopeMembersByIndexes db vr user gInfo scopeGMId indexesInGroup = do
|
||||
#if defined(dbPostgres)
|
||||
let GroupInfo {groupId} = gInfo
|
||||
map (toContactMember vr user) <$>
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group IN ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?)")
|
||||
(groupId, In indexesInGroup, GRModerator, GRAdmin, GROwner, scopeGMId)
|
||||
#else
|
||||
rights <$> mapM (runExceptT . getSupportScopeMemberByIndex db vr user gInfo scopeGMId) indexesInGroup
|
||||
#endif
|
||||
|
||||
getGroupModerators :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupModerators db vr user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
@@ -1141,12 +1187,12 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, sent_inv_queue_info, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser, invitedByGroupMemberId)
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, GSMemInvited, Binary B.empty, fromInvitedBy userContactId IBUser, invitedByGroupMemberId)
|
||||
:. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -1169,12 +1215,12 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, GSMemInvited, Binary B.empty, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
:. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -1212,12 +1258,12 @@ createJoiningMember
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, member_welcome_shared_msg_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, memberStatus, Binary B.empty, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
:. (userId, ldn, Nothing :: (Maybe Int64), profileId, cReqXContactId_, welcomeMsgId_, currentTs, currentTs)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -1291,12 +1337,12 @@ createBusinessRequestGroup
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, MemberId memId, GRMember, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
( (groupId, indexInGroup, MemberId memId, GRMember, GCInviteeMember, GSMemAccepted, Binary B.empty, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -1497,12 +1543,12 @@ createNewMember_
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
(group_id, index_in_group, member_id, member_role, member_category, member_status, member_restriction, invited_by, invited_by_group_member_id,
|
||||
(group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, member_restriction, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, memRestriction, invitedById, memInvitedByGroupMemberId)
|
||||
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty, memRestriction, invitedById, memInvitedByGroupMemberId)
|
||||
:. (userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -1561,27 +1607,196 @@ updateGroupMemberRole :: DB.Connection -> User -> GroupMember -> GroupMemberRole
|
||||
updateGroupMemberRole db User {userId} GroupMember {groupMemberId} memRole =
|
||||
DB.execute db "UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?" (memRole, userId, groupMemberId)
|
||||
|
||||
createIntroductions :: DB.Connection -> VersionChat -> [GroupMember] -> GroupMember -> IO [GroupMemberIntro]
|
||||
createIntroductions db chatV members toMember = do
|
||||
let reMembers = filter (\m -> memberCurrent m && groupMemberId' m /= groupMemberId' toMember) members
|
||||
if null reMembers
|
||||
then pure []
|
||||
else do
|
||||
createIntroductions :: DB.Connection -> VersionChat -> [GroupMember] -> GroupMember -> IO [GroupMember]
|
||||
createIntroductions db chatV reMembers toMember
|
||||
| null reMembers = pure []
|
||||
| otherwise = do
|
||||
currentTs <- getCurrentTime
|
||||
mapM (insertIntro_ currentTs) reMembers
|
||||
catMaybes <$> mapM (createIntro_ currentTs) reMembers
|
||||
where
|
||||
insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro
|
||||
insertIntro_ ts reMember = do
|
||||
DB.execute
|
||||
createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMember)
|
||||
createIntro_ ts reMember =
|
||||
-- when members connect concurrently, host would try to create introductions between them in both directions;
|
||||
-- this check avoids creating second (redundant) introduction
|
||||
checkInverseIntro >>= \case
|
||||
Just _ -> pure Nothing
|
||||
Nothing -> do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
pure $ Just reMember
|
||||
where
|
||||
checkInverseIntro :: IO (Maybe Int64)
|
||||
checkInverseIntro =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1"
|
||||
(groupMemberId' toMember, groupMemberId' reMember)
|
||||
|
||||
-- Create introductions for members without vectors and update vectors for members with vectors.
|
||||
-- Partitioning and updates happen in same transaction to avoid race conditions.
|
||||
createIntrosOrUpdateVectors :: DB.Connection -> VersionRangeChat -> [GroupMember] -> GroupMember -> IO [GroupMember]
|
||||
createIntrosOrUpdateVectors db vr reMembers toMember
|
||||
| null reMembers = pure []
|
||||
| otherwise = do
|
||||
(memsWithVec, memsWithoutVec) <- partitionByVector reMembers
|
||||
let GroupMember {indexInGroup} = toMember
|
||||
setMembersVectorsNewRelation db memsWithVec indexInGroup IDSubjectIntroduced MRIntroduced
|
||||
memsWithoutVec' <- createIntroductions db (maxVersion vr) memsWithoutVec toMember
|
||||
pure $ memsWithoutVec' <> memsWithVec
|
||||
where
|
||||
partitionByVector :: [GroupMember] -> IO ([GroupMember], [GroupMember])
|
||||
#if defined(dbPostgres)
|
||||
partitionByVector members = do
|
||||
let memberIds = map groupMemberId' members
|
||||
-- Lock rows first to ensure partitioning doesn't change in case of concurrent updates
|
||||
_ :: [Only Int] <-
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM group_members WHERE group_member_id IN ? FOR UPDATE"
|
||||
(Only $ In memberIds)
|
||||
memberIdsWithVec <- S.fromList . map fromOnly <$>
|
||||
DB.query
|
||||
db
|
||||
"SELECT group_member_id FROM group_members WHERE group_member_id IN ? AND member_relations_vector IS NOT NULL"
|
||||
(Only $ In memberIds)
|
||||
pure $ partition (\m -> groupMemberId' m `S.member` memberIdsWithVec) members
|
||||
#else
|
||||
partitionByVector = foldrM checkMember ([], [])
|
||||
where
|
||||
checkMember m (withVec, withoutVec) = do
|
||||
hasVec <- isJust <$> maybeFirstRow fromOnly
|
||||
(DB.query db "SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL" (Only $ groupMemberId' m) :: IO [Only Int64])
|
||||
pure $ if hasVec then (m : withVec, withoutVec) else (withVec, m : withoutVec)
|
||||
#endif
|
||||
|
||||
setMemberVectorNewRelations :: DB.Connection -> GroupMember -> [(Int64, (IntroductionDirection, MemberRelation))] -> IO ()
|
||||
setMemberVectorNewRelations db GroupMember {groupMemberId} relations = do
|
||||
v_ <- maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
#if defined(dbPostgres)
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? FOR UPDATE"
|
||||
#else
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
|
||||
#endif
|
||||
(Only groupMemberId)
|
||||
let v' = setNewRelations relations $ fromMaybe B.empty v_
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|]
|
||||
(Binary v', currentTs, groupMemberId)
|
||||
|
||||
setMembersVectorsNewRelation :: DB.Connection -> [GroupMember] -> Int64 -> IntroductionDirection -> MemberRelation -> IO ()
|
||||
setMembersVectorsNewRelation db members idx dir status = do
|
||||
currentTs <- getCurrentTime
|
||||
#if defined(dbPostgres)
|
||||
let memberIds = map groupMemberId' members
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE group_members SET member_relations_vector = set_member_vector_new_relation(member_relations_vector, ?, ?, ?), updated_at = ? WHERE group_member_id IN ?"
|
||||
(idx, toIntroDirInt dir, toRelationInt status, currentTs, In memberIds)
|
||||
#else
|
||||
forM_ members $ \GroupMember {groupMemberId} ->
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE group_members SET member_relations_vector = set_member_vector_new_relation(member_relations_vector, ?, ?, ?), updated_at = ? WHERE group_member_id = ?"
|
||||
(idx, toIntroDirInt dir, toRelationInt status, currentTs, groupMemberId)
|
||||
#endif
|
||||
|
||||
setMemberVectorRelationConnected :: DB.Connection -> GroupMember -> GroupMember -> MemberRelation -> ExceptT StoreError IO ()
|
||||
setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {indexInGroup} newStatus = do
|
||||
when (newStatus /= MRSubjectConnected && newStatus /= MRReferencedConnected) $
|
||||
throwError SEInvalidMemberRelationUpdate
|
||||
v <- ExceptT $
|
||||
firstRow fromOnly (SEMemberRelationsVectorNotFound groupMemberId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
introId <- insertedRowId db
|
||||
pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending}
|
||||
#if defined(dbPostgres)
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL FOR UPDATE"
|
||||
#else
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL"
|
||||
#endif
|
||||
(Only groupMemberId)
|
||||
let v' = setRelationConnected indexInGroup newStatus v
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $ DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|]
|
||||
(Binary v', currentTs, groupMemberId)
|
||||
|
||||
migrateGetMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString
|
||||
migrateGetMemberRelationsVector db m@GroupMember {groupMemberId} = do
|
||||
liftIO $ migrateMemberRelationsVector db m
|
||||
ExceptT . firstRow fromOnly (SEGroupMemberNotFound groupMemberId) $
|
||||
DB.query
|
||||
db
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
|
||||
(Only groupMemberId)
|
||||
|
||||
migrateMemberRelationsVector :: DB.Connection -> GroupMember -> IO ()
|
||||
migrateMemberRelationsVector db GroupMember {groupMemberId} =
|
||||
migrateMemberRelationsVector' db groupMemberId
|
||||
|
||||
migrateMemberRelationsVector' :: DB.Connection -> GroupMemberId -> IO ()
|
||||
migrateMemberRelationsVector' db groupMemberId = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $ do
|
||||
#if defined(dbPostgres)
|
||||
-- Lock the row first to ensure computation runs only after lock is acquired
|
||||
_ :: [Only Int] <-
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NULL FOR UPDATE"
|
||||
(Only groupMemberId)
|
||||
#endif
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET
|
||||
member_relations_vector = (
|
||||
SELECT migrate_relations_vector(idx, direction, intro_status)
|
||||
FROM (
|
||||
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = group_members.group_member_id
|
||||
UNION ALL
|
||||
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = group_members.group_member_id
|
||||
) AS relations
|
||||
),
|
||||
updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
AND member_relations_vector IS NULL
|
||||
|]
|
||||
(currentTs, groupMemberId)
|
||||
|
||||
getMemberRelationsVector_ :: DB.Connection -> GroupMember -> IO (Maybe ByteString)
|
||||
getMemberRelationsVector_ db GroupMember {groupMemberId} =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
|
||||
(Only groupMemberId)
|
||||
|
||||
updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO ()
|
||||
updateIntroStatus db introId introStatus = do
|
||||
@@ -1595,9 +1810,9 @@ updateIntroStatus db introId introStatus = do
|
||||
|]
|
||||
(introStatus, currentTs, introId)
|
||||
|
||||
getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> ExceptT StoreError IO GroupMemberIntro
|
||||
getIntroduction db reMember toMember = ExceptT $
|
||||
firstRow toIntro SEIntroNotFound $
|
||||
getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> IO (Maybe GroupMemberIntro)
|
||||
getIntroduction db reMember toMember =
|
||||
maybeFirstRow toIntro $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -1619,106 +1834,6 @@ getIntroducedGroupMemberIds db invitee =
|
||||
"SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ?"
|
||||
(Only $ groupMemberId' invitee)
|
||||
|
||||
getForwardIntroducedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||
getForwardIntroducedMembers db vr user invitee highlyAvailable = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' invitee
|
||||
query
|
||||
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
||||
| otherwise =
|
||||
DB.query
|
||||
db
|
||||
(q <> " AND intro_chat_protocol_version >= ?")
|
||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion)
|
||||
q =
|
||||
[sql|
|
||||
SELECT re_group_member_id
|
||||
FROM group_member_intros
|
||||
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||
|]
|
||||
|
||||
-- for support scope we don't need to filter by intro_chat_protocol_version for non highly available client,
|
||||
-- as we will filter moderators supporting this feature by a higher version (as opposed to getForwardIntroducedMembers)
|
||||
getForwardIntroducedModerators :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO [GroupMember]
|
||||
getForwardIntroducedModerators db vr user@User {userContactId} invitee = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' invitee
|
||||
query =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT i.re_group_member_id
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = ? AND i.intro_status NOT IN (?,?,?)
|
||||
AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)
|
||||
|]
|
||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, userContactId, GRModerator, GRAdmin, GROwner)
|
||||
|
||||
getForwardInvitedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||
getForwardInvitedMembers db vr user forwardMember highlyAvailable = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' forwardMember
|
||||
query
|
||||
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
||||
| otherwise =
|
||||
DB.query
|
||||
db
|
||||
(q <> " AND intro_chat_protocol_version >= ?")
|
||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion)
|
||||
q =
|
||||
[sql|
|
||||
SELECT to_group_member_id
|
||||
FROM group_member_intros
|
||||
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||
|]
|
||||
|
||||
-- for support scope we don't need to filter by intro_chat_protocol_version for non highly available client,
|
||||
-- as we will filter moderators supporting this feature by a higher version (as opposed to getForwardInvitedMembers)
|
||||
getForwardInvitedModerators :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO [GroupMember]
|
||||
getForwardInvitedModerators db vr user@User {userContactId} forwardMember = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' forwardMember
|
||||
query =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT i.to_group_member_id
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = ? AND i.intro_status NOT IN (?,?,?)
|
||||
AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)
|
||||
|]
|
||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, userContactId, GRModerator, GRAdmin, GROwner)
|
||||
|
||||
getForwardScopeMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMemberId -> IO (Maybe GroupMember)
|
||||
getForwardScopeMember db vr user GroupMember {groupMemberId = sendingGMId} scopeGMId = do
|
||||
(introExists_ :: Maybe Int64) <-
|
||||
liftIO $ maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT 1
|
||||
FROM group_member_intros
|
||||
WHERE
|
||||
(
|
||||
(re_group_member_id = ? AND to_group_member_id = ?) OR
|
||||
(re_group_member_id = ? AND to_group_member_id = ?)
|
||||
)
|
||||
AND intro_status NOT IN (?,?,?)
|
||||
LIMIT 1
|
||||
|]
|
||||
(sendingGMId, scopeGMId, scopeGMId, sendingGMId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
||||
pure introExists_ $>> (eitherToMaybe <$> runExceptT (getGroupMemberById db vr user scopeGMId))
|
||||
|
||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||
createIntroReMember
|
||||
db
|
||||
@@ -2505,12 +2620,12 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, indexInGroup, memberId, GRAuthor, GCPreMember, GSMemUnknown, fromInvitedBy userContactId IBUnknown)
|
||||
( (groupId, indexInGroup, memberId, GRAuthor, GCPreMember, GSMemUnknown, Binary B.empty, fromInvitedBy userContactId IBUnknown)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
@@ -2608,6 +2723,25 @@ getUserGroupsToExpire db User {userId} globalTTL =
|
||||
where
|
||||
cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL"
|
||||
|
||||
hasMembersWithoutVector :: DB.Connection -> IO Bool
|
||||
hasMembersWithoutVector db =
|
||||
fromOnly . head
|
||||
<$> DB.query_
|
||||
db
|
||||
"SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1)"
|
||||
|
||||
getGMsWithoutVectorIds :: DB.Connection -> IO [GroupMemberId]
|
||||
getGMsWithoutVectorIds db =
|
||||
map fromOnly <$>
|
||||
DB.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT group_member_id
|
||||
FROM group_members
|
||||
WHERE member_relations_vector IS NULL
|
||||
LIMIT 1000
|
||||
|]
|
||||
|
||||
updateGroupAlias :: DB.Connection -> UserId -> GroupInfo -> LocalAlias -> IO GroupInfo
|
||||
updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do
|
||||
updatedAt <- getCurrentTime
|
||||
|
||||
@@ -22,6 +22,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250922_remove_unused_connection
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
|
||||
-- import Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -44,6 +45,7 @@ schemaMigrations =
|
||||
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
|
||||
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
|
||||
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector)
|
||||
-- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -6,10 +6,91 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
-- This migration creates custom aggregate function migrate_relations_vector(idx, direction, intro_status).
|
||||
-- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2).
|
||||
--
|
||||
-- Vector byte encoding: 4 reserved | 1 direction | 3 status
|
||||
-- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced
|
||||
-- Status values: 0 = MRNew, 1 = MRIntroduced, 2 = MRSubjectConnected, 3 = MRReferencedConnected, 4 = MRConnected
|
||||
--
|
||||
-- The aggregate transforms intro_status into relation status:
|
||||
-- - intro_status 'new'/'sent'/'rcv'/'fwd': MRIntroduced (1)
|
||||
-- - intro_status 're-con': if direction=0 then MRSubjectConnected (2), else MRReferencedConnected (3)
|
||||
-- - intro_status 'to-con': if direction=0 then MRReferencedConnected (3), else MRSubjectConnected (2)
|
||||
-- - intro_status 'con': MRConnected (4)
|
||||
--
|
||||
-- Final byte combines direction and status: byte = (direction << 3) | status
|
||||
|
||||
m20251117_member_relations_vector :: Text
|
||||
m20251117_member_relations_vector =
|
||||
T.pack
|
||||
[r|
|
||||
CREATE FUNCTION set_member_vector_new_relation(v BYTEA, idx BIGINT, direction INT, status INT)
|
||||
RETURNS BYTEA AS $$
|
||||
DECLARE
|
||||
new_len INT;
|
||||
result BYTEA;
|
||||
byte_val INT;
|
||||
old_byte INT;
|
||||
BEGIN
|
||||
IF idx < 0 THEN
|
||||
RETURN v;
|
||||
END IF;
|
||||
IF idx < length(v) THEN
|
||||
old_byte := get_byte(v, idx::INT);
|
||||
ELSE
|
||||
old_byte := 0;
|
||||
END IF;
|
||||
byte_val := (old_byte & x'F0'::INT) | (direction * 8) | status;
|
||||
new_len := GREATEST(length(v), idx + 1);
|
||||
IF new_len > length(v) THEN
|
||||
result := v || (SELECT string_agg('\x00'::BYTEA, ''::BYTEA) FROM generate_series(1, new_len - length(v)));
|
||||
ELSE
|
||||
result := v;
|
||||
END IF;
|
||||
result := set_byte(result, idx::INT, byte_val);
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
CREATE FUNCTION migrate_relations_vector_step(state BYTEA, idx BIGINT, direction INT, intro_status TEXT)
|
||||
RETURNS BYTEA AS $$
|
||||
DECLARE
|
||||
new_len INT;
|
||||
result BYTEA;
|
||||
status INT;
|
||||
byte_val INT;
|
||||
BEGIN
|
||||
IF idx < 0 THEN
|
||||
RETURN state;
|
||||
END IF;
|
||||
IF intro_status = 're-con' THEN
|
||||
IF direction = 0 THEN status := 2; ELSE status := 3; END IF;
|
||||
ELSIF intro_status = 'to-con' THEN
|
||||
IF direction = 0 THEN status := 3; ELSE status := 2; END IF;
|
||||
ELSIF intro_status = 'con' THEN
|
||||
status := 4;
|
||||
ELSE
|
||||
status := 1;
|
||||
END IF;
|
||||
byte_val := (direction * 8) + status;
|
||||
new_len := GREATEST(length(state), idx + 1);
|
||||
IF new_len > length(state) THEN
|
||||
result := state || (SELECT string_agg('\x00'::BYTEA, ''::BYTEA) FROM generate_series(1, new_len - length(state)));
|
||||
ELSE
|
||||
result := state;
|
||||
END IF;
|
||||
result := set_byte(result, idx::INT, byte_val);
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
CREATE AGGREGATE migrate_relations_vector(BIGINT, INT, TEXT) (
|
||||
SFUNC = migrate_relations_vector_step,
|
||||
STYPE = BYTEA,
|
||||
INITCOND = ''
|
||||
);
|
||||
|
||||
ALTER TABLE group_members ADD COLUMN index_in_group BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE groups ADD COLUMN member_index BIGINT NOT NULL DEFAULT 0;
|
||||
@@ -46,12 +127,28 @@ SET member_index = COALESCE((
|
||||
FROM group_members
|
||||
WHERE group_members.group_id = g.group_id
|
||||
), 0);
|
||||
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ''::BYTEA
|
||||
WHERE group_id IN (
|
||||
SELECT mu.group_id
|
||||
FROM group_members mu
|
||||
WHERE mu.member_category = 'user'
|
||||
AND (
|
||||
mu.member_role NOT IN ('admin', 'owner')
|
||||
OR mu.member_status IN ('removed', 'left', 'deleted')
|
||||
)
|
||||
);
|
||||
|]
|
||||
|
||||
down_m20251117_member_relations_vector :: Text
|
||||
down_m20251117_member_relations_vector =
|
||||
T.pack
|
||||
[r|
|
||||
DROP AGGREGATE migrate_relations_vector(BIGINT, INT, TEXT);
|
||||
DROP FUNCTION migrate_relations_vector_step(BYTEA, BIGINT, INT, TEXT);
|
||||
DROP FUNCTION set_member_vector_new_relation(BYTEA, BIGINT, INT, INT);
|
||||
|
||||
DROP INDEX idx_group_members_group_id_index_in_group;
|
||||
|
||||
ALTER TABLE group_members DROP COLUMN index_in_group;
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
-- Build member_relations_vector for all members that don't have it yet.
|
||||
-- Uses custom aggregate function migrate_relations_vector defined in M20251117_member_relations_vector.
|
||||
--
|
||||
-- Query returns (idx, direction, intro_status) for each introduction:
|
||||
-- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member
|
||||
-- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it
|
||||
|
||||
-- TODO [relations vector] drop group_member_intros in the end of migration
|
||||
m20251128_member_relations_vector_stage_2 :: Text
|
||||
m20251128_member_relations_vector_stage_2 =
|
||||
T.pack
|
||||
[r|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = (
|
||||
SELECT migrate_relations_vector(idx, direction, intro_status)
|
||||
FROM (
|
||||
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = group_members.group_member_id
|
||||
UNION ALL
|
||||
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = group_members.group_member_id
|
||||
) AS relations
|
||||
)
|
||||
WHERE member_relations_vector IS NULL;
|
||||
|]
|
||||
|
||||
-- TODO [relations vector] re-create group_member_intros
|
||||
down_m20251128_member_relations_vector_stage_2 :: Text
|
||||
down_m20251128_member_relations_vector_stage_2 =
|
||||
T.pack
|
||||
[r|
|
||||
|
||||
|]
|
||||
@@ -34,6 +34,41 @@ $$;
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION test_chat_schema.migrate_relations_vector_step(state bytea, idx bigint, direction integer, intro_status text) RETURNS bytea
|
||||
LANGUAGE plpgsql IMMUTABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
new_len INT;
|
||||
result BYTEA;
|
||||
status INT;
|
||||
byte_val INT;
|
||||
BEGIN
|
||||
IF idx < 0 THEN
|
||||
RETURN state;
|
||||
END IF;
|
||||
IF intro_status = 're-con' THEN
|
||||
IF direction = 0 THEN status := 2; ELSE status := 3; END IF;
|
||||
ELSIF intro_status = 'to-con' THEN
|
||||
IF direction = 0 THEN status := 3; ELSE status := 2; END IF;
|
||||
ELSIF intro_status = 'con' THEN
|
||||
status := 4;
|
||||
ELSE
|
||||
status := 1;
|
||||
END IF;
|
||||
byte_val := (direction * 8) + status;
|
||||
new_len := GREATEST(length(state), idx + 1);
|
||||
IF new_len > length(state) THEN
|
||||
result := state || (SELECT string_agg('\x00'::BYTEA, ''::BYTEA) FROM generate_series(1, new_len - length(state)));
|
||||
ELSE
|
||||
result := state;
|
||||
END IF;
|
||||
result := set_byte(result, idx::INT, byte_val);
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION test_chat_schema.on_group_members_delete_update_summary() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -85,6 +120,45 @@ END;
|
||||
$$;
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION test_chat_schema.set_member_vector_new_relation(v bytea, idx bigint, direction integer, status integer) RETURNS bytea
|
||||
LANGUAGE plpgsql IMMUTABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
new_len INT;
|
||||
result BYTEA;
|
||||
byte_val INT;
|
||||
old_byte INT;
|
||||
BEGIN
|
||||
IF idx < 0 THEN
|
||||
RETURN v;
|
||||
END IF;
|
||||
IF idx < length(v) THEN
|
||||
old_byte := get_byte(v, idx::INT);
|
||||
ELSE
|
||||
old_byte := 0;
|
||||
END IF;
|
||||
byte_val := (old_byte & x'F0'::INT) | (direction * 8) | status;
|
||||
new_len := GREATEST(length(v), idx + 1);
|
||||
IF new_len > length(v) THEN
|
||||
result := v || (SELECT string_agg('\x00'::BYTEA, ''::BYTEA) FROM generate_series(1, new_len - length(v)));
|
||||
ELSE
|
||||
result := v;
|
||||
END IF;
|
||||
result := set_byte(result, idx::INT, byte_val);
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
|
||||
CREATE AGGREGATE test_chat_schema.migrate_relations_vector(bigint, integer, text) (
|
||||
SFUNC = test_chat_schema.migrate_relations_vector_step,
|
||||
STYPE = bytea,
|
||||
INITCOND = ''
|
||||
);
|
||||
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250922_remove_unused_connections
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
|
||||
-- import Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -290,6 +291,7 @@ schemaMigrations =
|
||||
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
|
||||
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
|
||||
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector)
|
||||
-- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -1,9 +1,77 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector where
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Database.SQLite3 (funcArgBlob, funcArgInt64, funcArgText, funcResultBlob)
|
||||
import Database.SQLite3.Bindings
|
||||
import Foreign.C.Types
|
||||
import Foreign.Ptr
|
||||
import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRelation (..), fromIntroDirInt, fromRelationInt, setNewRelation, setNewRelations)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Util (SQLiteFunc, SQLiteFuncFinal, mkSQLiteAggFinal, mkSQLiteAggStep, mkSQLiteFunc)
|
||||
|
||||
-- This module defines custom aggregate function migrate_relations_vector(idx, direction, intro_status).
|
||||
-- It is passed via DBOpts and registered on DB open.
|
||||
-- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2).
|
||||
--
|
||||
-- Vector byte encoding: 4 reserved | 1 direction | 3 status
|
||||
-- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced
|
||||
-- Status values: 0 = MRNew, 1 = MRIntroduced, 2 = MRSubjectConnected, 3 = MRReferencedConnected, 4 = MRConnected
|
||||
--
|
||||
-- The aggregate transforms intro_status into relation status:
|
||||
-- - intro_status 'new'/'sent'/'rcv'/'fwd': MRIntroduced (1)
|
||||
-- - intro_status 're-con': if direction=0 then MRSubjectConnected (2), else MRReferencedConnected (3)
|
||||
-- - intro_status 'to-con': if direction=0 then MRReferencedConnected (3), else MRSubjectConnected (2)
|
||||
-- - intro_status 'con': MRConnected (4)
|
||||
--
|
||||
-- The final function builds the vector using setNewRelations.
|
||||
|
||||
foreign export ccall "simplex_member_relations_step" sqliteMemberRelationsStep :: SQLiteFunc
|
||||
|
||||
foreign import ccall "&simplex_member_relations_step" sqliteMemberRelationsStepPtr :: FunPtr SQLiteFunc
|
||||
|
||||
foreign export ccall "simplex_member_relations_final" sqliteMemberRelationsFinal :: SQLiteFuncFinal
|
||||
|
||||
foreign import ccall "&simplex_member_relations_final" sqliteMemberRelationsFinalPtr :: FunPtr SQLiteFuncFinal
|
||||
|
||||
-- Step function for migrate_relations_vector aggregate.
|
||||
-- Accumulates (idx, direction, relation) tuples.
|
||||
sqliteMemberRelationsStep :: SQLiteFunc
|
||||
sqliteMemberRelationsStep = mkSQLiteAggStep [] $ \_ args acc -> do
|
||||
idx <- funcArgInt64 args 0
|
||||
direction <- fromIntroDirInt . fromIntegral <$> funcArgInt64 args 1
|
||||
introStatus <- funcArgText args 2
|
||||
let relation = introStatusToRelation direction introStatus
|
||||
pure $ (idx, (direction, relation)) : acc
|
||||
where
|
||||
introStatusToRelation dir status = case status of
|
||||
"re-con" -> if dir == IDSubjectIntroduced then MRSubjectConnected else MRReferencedConnected
|
||||
"to-con" -> if dir == IDSubjectIntroduced then MRReferencedConnected else MRSubjectConnected
|
||||
"con" -> MRConnected
|
||||
_ -> MRIntroduced -- 'new', 'sent', 'rcv', 'fwd'
|
||||
|
||||
-- Final function for migrate_relations_vector aggregate.
|
||||
-- Builds the vector from accumulated tuples using setNewRelations.
|
||||
sqliteMemberRelationsFinal :: SQLiteFuncFinal
|
||||
sqliteMemberRelationsFinal = mkSQLiteAggFinal [] $ \cxt acc -> funcResultBlob cxt $ setNewRelations acc B.empty
|
||||
|
||||
-- Non-aggregate function set_member_vector_new_relation(vector, idx, direction, status).
|
||||
-- Sets a new relation in the vector and returns the updated vector.
|
||||
|
||||
foreign export ccall "simplex_set_member_vector_new_relation" sqliteSetMemberVectorNewRelation :: SQLiteFunc
|
||||
|
||||
foreign import ccall "&simplex_set_member_vector_new_relation" sqliteSetMemberVectorNewRelationPtr :: FunPtr SQLiteFunc
|
||||
|
||||
sqliteSetMemberVectorNewRelation :: SQLiteFunc
|
||||
sqliteSetMemberVectorNewRelation = mkSQLiteFunc $ \cxt args -> do
|
||||
v <- funcArgBlob args 0
|
||||
idx <- funcArgInt64 args 1
|
||||
direction <- fromIntroDirInt . fromIntegral <$> funcArgInt64 args 2
|
||||
status <- fromRelationInt . fromIntegral <$> funcArgInt64 args 3
|
||||
funcResultBlob cxt $ setNewRelation idx direction status v
|
||||
|
||||
m20251117_member_relations_vector :: Query
|
||||
m20251117_member_relations_vector =
|
||||
@@ -46,6 +114,18 @@ SET member_index = COALESCE((
|
||||
FROM group_members
|
||||
WHERE group_members.group_id = g.group_id
|
||||
), 0);
|
||||
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = x''
|
||||
WHERE group_id IN (
|
||||
SELECT mu.group_id
|
||||
FROM group_members mu
|
||||
WHERE mu.member_category = 'user'
|
||||
AND (
|
||||
mu.member_role NOT IN (CAST('admin' AS BLOB), CAST('owner' AS BLOB))
|
||||
OR mu.member_status IN ('removed', 'left', 'deleted')
|
||||
)
|
||||
);
|
||||
|]
|
||||
|
||||
down_m20251117_member_relations_vector :: Query
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
-- Build member_relations_vector for all members that don't have it yet.
|
||||
-- Uses custom aggregate function migrate_relations_vector defined in M20251117_member_relations_vector.
|
||||
--
|
||||
-- Query returns (idx, direction, intro_status) for each introduction:
|
||||
-- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member
|
||||
-- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it
|
||||
|
||||
-- TODO [relations vector] drop group_member_intros in the end of migration
|
||||
m20251128_member_relations_vector_stage_2 :: Query
|
||||
m20251128_member_relations_vector_stage_2 =
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = (
|
||||
SELECT migrate_relations_vector(idx, direction, intro_status)
|
||||
FROM (
|
||||
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = group_members.group_member_id
|
||||
UNION ALL
|
||||
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = group_members.group_member_id
|
||||
)
|
||||
)
|
||||
WHERE member_relations_vector IS NULL;
|
||||
|]
|
||||
|
||||
-- TODO [relations vector] re-create group_member_intros
|
||||
down_m20251128_member_relations_vector_stage_2 :: Query
|
||||
down_m20251128_member_relations_vector_stage_2 =
|
||||
[sql|
|
||||
|
||||
|]
|
||||
@@ -24,10 +24,10 @@ SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -76,10 +76,10 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, sent_inv_queue_info, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -260,9 +260,9 @@ SEARCH users USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -293,10 +293,10 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -327,10 +327,10 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, member_welcome_shared_msg_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -492,9 +492,9 @@ SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -525,10 +525,10 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -989,19 +989,12 @@ Query:
|
||||
|
||||
Plan:
|
||||
|
||||
Query:
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
(group_id, index_in_group, member_id, member_role, member_category, member_status, member_restriction, invited_by, invited_by_group_member_id,
|
||||
(group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, member_restriction, invited_by, invited_by_group_member_id,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -1191,24 +1184,6 @@ SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_con
|
||||
SEARCH ct USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT 1
|
||||
FROM group_member_intros
|
||||
WHERE
|
||||
(
|
||||
(re_group_member_id = ? AND to_group_member_id = ?) OR
|
||||
(re_group_member_id = ? AND to_group_member_id = ?)
|
||||
)
|
||||
AND intro_status NOT IN (?,?,?)
|
||||
LIMIT 1
|
||||
|
||||
Plan:
|
||||
MULTI-INDEX OR
|
||||
INDEX 1
|
||||
SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?)
|
||||
INDEX 2
|
||||
SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?)
|
||||
|
||||
Query:
|
||||
SELECT 1 FROM users
|
||||
WHERE (user_id = ? AND local_display_name = ?)
|
||||
@@ -1455,28 +1430,6 @@ Plan:
|
||||
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH i USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT i.re_group_member_id
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = ? AND i.intro_status NOT IN (?,?,?)
|
||||
AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH i USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT i.to_group_member_id
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = ? AND i.intro_status NOT IN (?,?,?)
|
||||
AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH i USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT member_status
|
||||
FROM group_members
|
||||
@@ -1645,10 +1598,10 @@ SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO group_members
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by,
|
||||
( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by,
|
||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
|
||||
@@ -3509,6 +3462,14 @@ Plan:
|
||||
SEARCH f USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH i USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT index_in_group, member_relations_vector
|
||||
FROM group_members
|
||||
WHERE local_display_name = ?
|
||||
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query:
|
||||
SELECT m.group_member_id
|
||||
FROM group_members m
|
||||
@@ -3559,14 +3520,6 @@ SEARCH r USING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) LEFT-JOIN
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT re_group_member_id
|
||||
FROM group_member_intros
|
||||
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||
AND intro_chat_protocol_version >= ?
|
||||
Plan:
|
||||
SEARCH group_member_intros USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
|
||||
Query:
|
||||
SELECT reaction
|
||||
FROM chat_item_reactions
|
||||
@@ -3655,14 +3608,6 @@ Query:
|
||||
Plan:
|
||||
SEARCH protocol_servers USING INDEX idx_smp_servers_user_id (user_id=?)
|
||||
|
||||
Query:
|
||||
SELECT to_group_member_id
|
||||
FROM group_member_intros
|
||||
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||
AND intro_chat_protocol_version >= ?
|
||||
Plan:
|
||||
SEARCH group_member_intros USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
|
||||
Query:
|
||||
SELECT usage_conditions_id, conditions_commit, notified_at, created_at
|
||||
FROM usage_conditions
|
||||
@@ -3782,6 +3727,40 @@ SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_
|
||||
LIST SUBQUERY 2
|
||||
SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET
|
||||
member_relations_vector = (
|
||||
SELECT migrate_relations_vector(idx, direction, intro_status)
|
||||
FROM (
|
||||
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.to_group_member_id
|
||||
WHERE i.re_group_member_id = group_members.group_member_id
|
||||
UNION ALL
|
||||
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
|
||||
FROM group_member_intros i
|
||||
JOIN group_members m ON m.group_member_id = i.re_group_member_id
|
||||
WHERE i.to_group_member_id = group_members.group_member_id
|
||||
) AS relations
|
||||
),
|
||||
updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
AND member_relations_vector IS NULL
|
||||
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
CORRELATED SCALAR SUBQUERY 3
|
||||
CO-ROUTINE relations
|
||||
COMPOUND QUERY
|
||||
LEFT-MOST SUBQUERY
|
||||
SEARCH i USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
UNION ALL
|
||||
SEARCH i USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SCAN relations
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ?
|
||||
@@ -3790,6 +3769,14 @@ Query:
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?
|
||||
WHERE local_display_name = ?
|
||||
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query:
|
||||
UPDATE groups
|
||||
SET member_index = member_index + 1
|
||||
@@ -4739,12 +4726,12 @@ Plan:
|
||||
SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_member_intros
|
||||
SET intro_status = ?, updated_at = ?
|
||||
WHERE group_member_intro_id = ?
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH group_member_intros USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
@@ -5133,6 +5120,44 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
|
||||
m.created_at, m.updated_at,
|
||||
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.group_member_id = m.group_member_id
|
||||
WHERE m.group_id = ? AND m.index_in_group = ?
|
||||
Plan:
|
||||
SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=? AND index_in_group=?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
|
||||
m.created_at, m.updated_at,
|
||||
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.group_member_id = m.group_member_id
|
||||
WHERE m.group_id = ? AND m.index_in_group = ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?)
|
||||
Plan:
|
||||
SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=? AND index_in_group=?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
@@ -5209,25 +5234,6 @@ SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
|
||||
m.created_at, m.updated_at,
|
||||
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.group_member_id = m.group_member_id
|
||||
WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND index_in_group < ?
|
||||
Plan:
|
||||
SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=? AND index_in_group<?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
@@ -6083,6 +6089,10 @@ Plan:
|
||||
Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT 1 FROM settings WHERE user_id = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?)
|
||||
@@ -6113,6 +6123,12 @@ SCAN CONSTANT ROW
|
||||
SCALAR SUBQUERY 1
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
|
||||
Query: SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1)
|
||||
Plan:
|
||||
SCAN CONSTANT ROW
|
||||
SCALAR SUBQUERY 1
|
||||
SCAN group_members
|
||||
|
||||
Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ?
|
||||
Plan:
|
||||
SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_conditions_commit (conditions_commit=? AND server_operator_id=?)
|
||||
@@ -6281,6 +6297,14 @@ Query: SELECT max(active_order) FROM users
|
||||
Plan:
|
||||
SEARCH users
|
||||
|
||||
Query: SELECT member_relations_vector FROM group_members WHERE group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT member_xcontact_id, member_welcome_shared_msg_id FROM group_members WHERE user_id = ? AND group_id = ? AND group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -6297,10 +6321,6 @@ Query: SELECT quota_err_counter FROM connections WHERE user_id = ? AND connectio
|
||||
Plan:
|
||||
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_member_intros USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
|
||||
Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -6521,6 +6541,10 @@ Query: UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE grou
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE group_members SET member_relations_vector = set_member_vector_new_relation(member_relations_vector, ?, ?, ?), updated_at = ? WHERE group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
@@ -93,9 +93,12 @@ data StoreError
|
||||
| SEGroupNotFoundByName {groupName :: GroupName}
|
||||
| SEGroupMemberNameNotFound {groupId :: GroupId, groupMemberName :: ContactName}
|
||||
| SEGroupMemberNotFound {groupMemberId :: GroupMemberId}
|
||||
| SEGroupMemberNotFoundByIndex {groupMemberIndex :: Int64}
|
||||
| SEMemberRelationsVectorNotFound {groupMemberId :: GroupMemberId}
|
||||
| SEGroupHostMemberNotFound {groupId :: GroupId}
|
||||
| SEGroupMemberNotFoundByMemberId {memberId :: MemberId}
|
||||
| SEMemberContactGroupMemberNotFound {contactId :: ContactId}
|
||||
| SEInvalidMemberRelationUpdate
|
||||
| SEGroupWithoutUser
|
||||
| SEDuplicateGroupMember
|
||||
| SEGroupAlreadyJoined
|
||||
@@ -119,7 +122,6 @@ data StoreError
|
||||
| SEConnectionNotFoundById {connId :: Int64}
|
||||
| SEConnectionNotFoundByMemberId {groupMemberId :: GroupMemberId}
|
||||
| SEPendingConnectionNotFound {connId :: Int64}
|
||||
| SEIntroNotFound
|
||||
| SEUniqueID
|
||||
| SELargeMsg
|
||||
| SEInternalError {message :: String}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Simplex.Chat.Types.MemberRelations
|
||||
( MemberRelation (..),
|
||||
( IntroductionDirection (..),
|
||||
MemberRelation (..),
|
||||
toIntroDirInt,
|
||||
fromIntroDirInt,
|
||||
toRelationInt,
|
||||
fromRelationInt,
|
||||
getRelation,
|
||||
getRelation',
|
||||
getRelationsIndexes,
|
||||
setRelation,
|
||||
setRelations,
|
||||
setRelationConnected,
|
||||
setNewRelation,
|
||||
setNewRelations,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad
|
||||
import Data.Bits ((.&.), (.|.), complement)
|
||||
import Data.Bits (shiftL, shiftR, (.&.), (.|.), complement)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.ByteString.Internal (toForeignPtr, unsafeCreate)
|
||||
@@ -20,55 +30,129 @@ import Foreign.Marshal.Utils (copyBytes, fillBytes)
|
||||
import Foreign.Ptr (plusPtr)
|
||||
import Foreign.Storable (peekByteOff, pokeByteOff)
|
||||
|
||||
data IntroductionDirection
|
||||
= IDSubjectIntroduced -- Member described by vector (subject member, vector "owner") is introduced to member referenced in vector
|
||||
| IDReferencedIntroduced -- Member referenced in vector is introduced to subject member
|
||||
deriving (Eq, Show)
|
||||
|
||||
toIntroDirInt :: IntroductionDirection -> Word8
|
||||
toIntroDirInt = \case
|
||||
IDSubjectIntroduced -> 0
|
||||
IDReferencedIntroduced -> 1
|
||||
|
||||
fromIntroDirInt :: Word8 -> IntroductionDirection
|
||||
fromIntroDirInt = \case
|
||||
0 -> IDSubjectIntroduced
|
||||
1 -> IDReferencedIntroduced
|
||||
_ -> IDSubjectIntroduced
|
||||
|
||||
data MemberRelation
|
||||
= MRNew
|
||||
| MRIntroduced
|
||||
| MRConnected
|
||||
deriving (Eq, Show)
|
||||
| MRSubjectConnected -- Subject member notified about connection to referenced member
|
||||
| MRReferencedConnected -- Referenced member notified about connection to subject member
|
||||
| MRConnected -- Both members notified about connection
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
toRelationInt :: MemberRelation -> Word8
|
||||
toRelationInt = \case
|
||||
MRNew -> 0
|
||||
MRIntroduced -> 1
|
||||
MRConnected -> 2
|
||||
MRSubjectConnected -> 2
|
||||
MRReferencedConnected -> 3
|
||||
MRConnected -> 4
|
||||
|
||||
fromRelationInt :: Word8 -> MemberRelation
|
||||
fromRelationInt = \case
|
||||
0 -> MRNew
|
||||
1 -> MRIntroduced
|
||||
2 -> MRConnected
|
||||
2 -> MRSubjectConnected
|
||||
3 -> MRReferencedConnected
|
||||
4 -> MRConnected
|
||||
_ -> MRNew
|
||||
|
||||
-- Bit layout: 4 reserved | 1 direction | 3 status
|
||||
|
||||
-- | Get the relation status of a member at a given index from the relations vector.
|
||||
-- Returns 'MRNew' if the vector is not long enough (lazy initialization).
|
||||
getRelation :: Int64 -> ByteString -> MemberRelation
|
||||
getRelation i v
|
||||
| i < 0 || fromIntegral i >= B.length v = MRNew
|
||||
| otherwise = fromRelationInt $ (v `B.index` fromIntegral i) .&. relationMask
|
||||
getRelation i v = snd $ getRelation' i v
|
||||
|
||||
-- | Get both direction and status of a member at a given index from the relations vector.
|
||||
-- Returns (IDSubjectIntroduced, MRNew) if the vector is not long enough (lazy initialization).
|
||||
getRelation' :: Int64 -> ByteString -> (IntroductionDirection, MemberRelation)
|
||||
getRelation' i v
|
||||
| i < 0 || fromIntegral i >= B.length v = (IDSubjectIntroduced, MRNew)
|
||||
| otherwise =
|
||||
let b = v `B.index` fromIntegral i
|
||||
in (fromIntroDirInt $ (b .&. directionMask) `shiftR` 3, fromRelationInt $ b .&. statusMask)
|
||||
|
||||
-- | Get the indexes of members with the given relation status from the relations vector.
|
||||
getRelationsIndexes :: MemberRelation -> ByteString -> [Int64]
|
||||
getRelationsIndexes r v = [i | i <- [0 .. fromIntegral (B.length v) - 1], getRelation i v == r]
|
||||
|
||||
-- | Set the relation status of a member at a given index in the relations vector.
|
||||
-- Expands the vector lazily if needed (padding with zeros for 'MRNew' relation).
|
||||
-- Preserves the introduction direction. Expands the vector lazily if needed.
|
||||
setRelation :: Int64 -> MemberRelation -> ByteString -> ByteString
|
||||
setRelation i r v
|
||||
| i >= 0 = setRelations [(i, r)] v
|
||||
| otherwise = v
|
||||
|
||||
-- | Set multiple relations at once.
|
||||
-- Expands the vector lazily if needed (padding with zeros for 'MRNew' relation).
|
||||
-- | Set multiple relation statuses at once.
|
||||
-- Preserves the introduction direction. Expands the vector lazily if needed.
|
||||
setRelations :: [(Int64, MemberRelation)] -> ByteString -> ByteString
|
||||
setRelations [] v = v
|
||||
setRelations relations v =
|
||||
setRelations = setRelations_ $ \r b -> (b .&. complement statusMask) .|. toRelationInt r
|
||||
|
||||
-- | Set relation to connected state based on passed status and current status.
|
||||
-- newStatus should be MRSubjectConnected or MRReferencedConnected, otherwise returns vector unchanged.
|
||||
-- Logic:
|
||||
-- - if newStatus is complementary to oldStatus -> set MRConnected
|
||||
-- - if newStatus > oldStatus (by enum order) -> set newStatus
|
||||
-- - otherwise don't update
|
||||
setRelationConnected :: Int64 -> MemberRelation -> ByteString -> ByteString
|
||||
setRelationConnected i newStatus v
|
||||
| newStatus /= MRSubjectConnected && newStatus /= MRReferencedConnected = v
|
||||
| otherwise = case status' of
|
||||
Nothing -> v
|
||||
Just s -> setRelation i s v
|
||||
where
|
||||
oldStatus = getRelation i v
|
||||
status' = case (oldStatus, newStatus) of
|
||||
-- complementary statuses -> MRConnected
|
||||
(MRSubjectConnected, MRReferencedConnected) -> Just MRConnected
|
||||
(MRReferencedConnected, MRSubjectConnected) -> Just MRConnected
|
||||
-- newStatus > oldStatus -> set newStatus
|
||||
_ | newStatus > oldStatus -> Just newStatus
|
||||
| otherwise -> Nothing
|
||||
|
||||
-- | Set a new relation with both direction and status at a given index.
|
||||
-- Expands the vector lazily if needed.
|
||||
setNewRelation :: Int64 -> IntroductionDirection -> MemberRelation -> ByteString -> ByteString
|
||||
setNewRelation i dir r v
|
||||
| i >= 0 = setNewRelations [(i, (dir, r))] v
|
||||
| otherwise = v
|
||||
|
||||
-- | Set multiple new relations with both direction and status at once.
|
||||
-- Expands the vector lazily if needed.
|
||||
setNewRelations :: [(Int64, (IntroductionDirection, MemberRelation))] -> ByteString -> ByteString
|
||||
setNewRelations = setRelations_ $ \(dir, r) b -> (b .&. relationMask) .|. (toIntroDirInt dir `shiftL` 3) .|. toRelationInt r
|
||||
where
|
||||
relationMask = complement (statusMask .|. directionMask)
|
||||
|
||||
setRelations_ :: (r -> Word8 -> Word8) -> [(Int64, r)] -> ByteString -> ByteString
|
||||
setRelations_ _ [] v = v
|
||||
setRelations_ updateByte relations v =
|
||||
let (fp, off, len) = toForeignPtr v
|
||||
newLen = max len $ fromIntegral $ maximum (map fst relations) + 1
|
||||
in unsafeCreate newLen $ \ptr -> do
|
||||
withForeignPtr fp $ \vPtr -> copyBytes ptr (vPtr `plusPtr` off) len
|
||||
when (newLen > len) $ fillBytes (ptr `plusPtr` len) 0 (newLen - len)
|
||||
forM_ relations $ \(ix, r) -> when (ix >= 0) $ do
|
||||
forM_ relations $ \(ix, r) -> when (ix >= 0) $
|
||||
let i = fromIntegral ix
|
||||
b <- peekByteOff ptr i
|
||||
let b' = (b .&. complement relationMask) .|. toRelationInt r
|
||||
pokeByteOff ptr i b'
|
||||
in pokeByteOff ptr i . updateByte r =<< peekByteOff ptr i
|
||||
|
||||
relationMask :: Word8
|
||||
relationMask = 0x07 -- reserving 3 bits
|
||||
statusMask :: Word8
|
||||
statusMask = 0x07 -- bits 0-2
|
||||
|
||||
directionMask :: Word8
|
||||
directionMask = 0x08 -- bit 3
|
||||
|
||||
@@ -24,7 +24,6 @@ import Data.Function (on)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (groupBy, intercalate, intersperse, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
|
||||
|
||||
@@ -18,6 +18,7 @@ import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Monad (forM_, void, when)
|
||||
import Data.Bifunctor (second)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, isInfixOf)
|
||||
@@ -30,10 +31,12 @@ import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId)
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations (MemberRelation (..), setRelation)
|
||||
import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..))
|
||||
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.Encoding.String
|
||||
import Simplex.Messaging.Server.Env.STM hiding (subscriptions)
|
||||
import Simplex.Messaging.Transport
|
||||
@@ -1769,6 +1772,7 @@ testGroupDelayedModeration ps = do
|
||||
-- and forwarding client doesn't check compatibility)
|
||||
void $ withCCTransaction alice $ \db ->
|
||||
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
|
||||
updateGroupForwardingVectors alice "bob" "cath" MRConnected
|
||||
|
||||
cath #> "#team hi" -- message is pending for bob
|
||||
alice <# "#team cath> hi"
|
||||
@@ -1815,6 +1819,7 @@ testGroupDelayedModerationFullDelete ps = do
|
||||
-- and forwarding client doesn't check compatibility)
|
||||
void $ withCCTransaction alice $ \db ->
|
||||
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
|
||||
updateGroupForwardingVectors alice "bob" "cath" MRConnected
|
||||
|
||||
cath #> "#team hi" -- message is pending for bob
|
||||
alice <# "#team cath> hi"
|
||||
@@ -5018,7 +5023,7 @@ testGroupMsgForwardReport =
|
||||
|
||||
setupGroupForwarding :: TestCC -> TestCC -> TestCC -> IO ()
|
||||
setupGroupForwarding host invitee1 invitee2 = do
|
||||
threadDelay 1000000 -- delay so intro_status doesn't get overwritten to connected
|
||||
threadDelay 1000000 -- delay so member relations don't get overwritten to connected
|
||||
|
||||
invitee1Name <- userName invitee1
|
||||
invitee2Name <- userName invitee2
|
||||
@@ -5050,15 +5055,60 @@ setupGroupForwarding host invitee1 invitee2 = do
|
||||
|]
|
||||
(invitee1Name, invitee2Name)
|
||||
|
||||
setupGroupForwardingVectors host invitee1 invitee2
|
||||
|
||||
setupGroupForwardingVectors :: TestCC -> TestCC -> TestCC -> IO ()
|
||||
setupGroupForwardingVectors host invitee1 invitee2 = do
|
||||
invitee1Name <- userName invitee1
|
||||
invitee2Name <- userName invitee2
|
||||
updateGroupForwardingVectors host invitee1Name invitee2Name MRIntroduced
|
||||
|
||||
updateGroupForwardingVectors :: TestCC -> String -> String -> MemberRelation -> IO ()
|
||||
updateGroupForwardingVectors host invitee1Name invitee2Name relation = do
|
||||
void $ withCCTransaction host $ \db -> do
|
||||
[(invitee1Index, invitee1Vec)] <- DB.query db
|
||||
[sql|
|
||||
SELECT index_in_group, member_relations_vector
|
||||
FROM group_members
|
||||
WHERE local_display_name = ?
|
||||
|]
|
||||
(Only invitee1Name)
|
||||
[(invitee2Index, invitee2Vec)] <- DB.query db
|
||||
[sql|
|
||||
SELECT index_in_group, member_relations_vector
|
||||
FROM group_members
|
||||
WHERE local_display_name = ?
|
||||
|]
|
||||
(Only invitee2Name)
|
||||
|
||||
let invitee1Vec' = setRelation invitee2Index relation (fromMaybe B.empty invitee1Vec)
|
||||
DB.execute db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?
|
||||
WHERE local_display_name = ?
|
||||
|]
|
||||
(Binary invitee1Vec', invitee1Name)
|
||||
|
||||
let invitee2Vec' = setRelation invitee1Index relation (fromMaybe B.empty invitee2Vec)
|
||||
DB.execute db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_relations_vector = ?
|
||||
WHERE local_display_name = ?
|
||||
|]
|
||||
(Binary invitee2Vec', invitee2Name)
|
||||
|
||||
testGroupMsgForwardDeduplicate :: HasCallStack => TestParams -> IO ()
|
||||
testGroupMsgForwardDeduplicate =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
threadDelay 1000000 -- delay so intro_status doesn't get overwritten to connected
|
||||
threadDelay 1000000 -- delay so member relations don't get overwritten to connected
|
||||
void $ withCCTransaction alice $ \db ->
|
||||
DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'"
|
||||
setupGroupForwardingVectors alice bob cath
|
||||
|
||||
bob #> "#team hi there"
|
||||
alice <# "#team bob> hi there"
|
||||
|
||||
+146
-55
@@ -29,21 +29,23 @@ memberRelationsTests = do
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
|
||||
it "reads multiple relations" $ do
|
||||
let vec = B.pack [0, 0, 1, 2]
|
||||
let vec = B.pack [0, 0, 1, 2, 3, 4]
|
||||
getRelation 0 vec `shouldBe` MRNew
|
||||
getRelation 1 vec `shouldBe` MRNew
|
||||
getRelation 2 vec `shouldBe` MRIntroduced
|
||||
getRelation 3 vec `shouldBe` MRConnected
|
||||
getRelation 3 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 4 vec `shouldBe` MRReferencedConnected
|
||||
getRelation 5 vec `shouldBe` MRConnected
|
||||
|
||||
it "reads multiple relations 2" $ do
|
||||
let vec = B.pack [1, 1, 0, 0, 2, 2, 0, 0]
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 1 vec `shouldBe` MRIntroduced
|
||||
getRelation 4 vec `shouldBe` MRConnected
|
||||
getRelation 5 vec `shouldBe` MRConnected
|
||||
getRelation 4 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 5 vec `shouldBe` MRSubjectConnected
|
||||
|
||||
it "ignore reserved bits" $ do
|
||||
let vec = B.pack [0xF9] -- 11111001
|
||||
let vec = B.pack [0xF1] -- reserved=1111, direction=0, status=001
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
|
||||
describe "setRelation" $ do
|
||||
@@ -56,9 +58,9 @@ memberRelationsTests = do
|
||||
vec `shouldBe` B.empty
|
||||
|
||||
it "expands vector to required length" $ do
|
||||
let vec = setRelation 5 MRConnected B.empty
|
||||
let vec = setRelation 5 MRSubjectConnected B.empty
|
||||
B.length vec `shouldBe` 6
|
||||
getRelation 5 vec `shouldBe` MRConnected
|
||||
getRelation 5 vec `shouldBe` MRSubjectConnected
|
||||
-- Other positions should be MRNew (0)
|
||||
getRelation 0 vec `shouldBe` MRNew
|
||||
getRelation 10 vec `shouldBe` MRNew
|
||||
@@ -69,21 +71,21 @@ memberRelationsTests = do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelation 1 MRIntroduced vec1
|
||||
-- Update: [01][10][00][00]
|
||||
let vec3 = setRelation 1 MRConnected vec2
|
||||
let vec3 = setRelation 1 MRSubjectConnected vec2
|
||||
getRelation 0 vec3 `shouldBe` MRIntroduced
|
||||
getRelation 1 vec3 `shouldBe` MRConnected
|
||||
getRelation 1 vec3 `shouldBe` MRSubjectConnected
|
||||
|
||||
it "updates relation in specific byte of multi-byte vector" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelation 10 MRConnected vec1
|
||||
let vec2 = setRelation 10 MRSubjectConnected vec1
|
||||
B.length vec2 `shouldBe` 11
|
||||
getRelation 0 vec2 `shouldBe` MRIntroduced
|
||||
getRelation 10 vec2 `shouldBe` MRConnected
|
||||
getRelation 10 vec2 `shouldBe` MRSubjectConnected
|
||||
forM_ [1..9] $ \i -> getRelation i vec2 `shouldBe` MRNew
|
||||
|
||||
it "handles setting relation at last position in byte" $ do
|
||||
let vec = setRelation 3 MRConnected B.empty
|
||||
getRelation 3 vec `shouldBe` MRConnected
|
||||
let vec = setRelation 3 MRSubjectConnected B.empty
|
||||
getRelation 3 vec `shouldBe` MRSubjectConnected
|
||||
|
||||
it "preserves vector when setting same value" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
@@ -91,12 +93,24 @@ memberRelationsTests = do
|
||||
vec2 `shouldBe` vec1
|
||||
getRelation 0 vec2 `shouldBe` MRIntroduced
|
||||
|
||||
it "preserves reserved bits" $ do
|
||||
let v = B.pack [0xF8] -- 11111000
|
||||
it "preserves reserved bits and direction" $ do
|
||||
let v = B.pack [0xF8] -- reserved=1111, direction=1, status=000
|
||||
getRelation 0 v `shouldBe` MRNew
|
||||
let v' = setRelation 0 MRIntroduced v
|
||||
getRelation 0 v' `shouldBe` MRIntroduced
|
||||
B.unpack v' `shouldBe` [0xF9] -- 11111001
|
||||
let v' = setRelation 0 MRConnected v
|
||||
getRelation 0 v' `shouldBe` MRConnected
|
||||
B.unpack v' `shouldBe` [0xFC] -- reserved=1111, direction=1, status=100
|
||||
|
||||
describe "setNewRelation" $ do
|
||||
it "sets new relation with direction" $ do
|
||||
let vec = setNewRelation 0 IDReferencedIntroduced MRSubjectConnected B.empty
|
||||
getRelation' 0 vec `shouldBe` (IDReferencedIntroduced, MRSubjectConnected)
|
||||
B.unpack vec `shouldBe` [0x0A] -- direction=1, status=010
|
||||
|
||||
it "preserves reserved bits" $ do
|
||||
let v = B.pack [0xF0] -- reserved=1111, direction=0, status=000
|
||||
let v' = setNewRelation 0 IDReferencedIntroduced MRConnected v
|
||||
getRelation 0 v' `shouldBe` MRConnected
|
||||
B.unpack v' `shouldBe` [0xFC] -- reserved=1111, direction=1, status=100
|
||||
|
||||
describe "setRelations" $ do
|
||||
it "returns same vector for empty list" $ do
|
||||
@@ -104,106 +118,183 @@ memberRelationsTests = do
|
||||
setRelations [] vec `shouldBe` vec
|
||||
|
||||
it "sets multiple relations in empty vector" $ do
|
||||
let updates = [(0, MRIntroduced), (1, MRConnected), (2, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (1, MRSubjectConnected), (2, MRReferencedConnected), (3, MRConnected)]
|
||||
let vec = setRelations updates B.empty
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 1 vec `shouldBe` MRConnected
|
||||
getRelation 2 vec `shouldBe` MRIntroduced
|
||||
getRelation 3 vec `shouldBe` MRNew -- Unset position
|
||||
getRelation 1 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 2 vec `shouldBe` MRReferencedConnected
|
||||
getRelation 3 vec `shouldBe` MRConnected
|
||||
getRelation 4 vec `shouldBe` MRNew -- Unset position
|
||||
|
||||
it "sets multiple relations 1" $ do
|
||||
let updates = [(0, MRIntroduced), (1, MRConnected), (2, MRConnected), (3, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (1, MRSubjectConnected), (2, MRSubjectConnected), (3, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
B.length vec `shouldBe` 4
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 1 vec `shouldBe` MRConnected
|
||||
getRelation 2 vec `shouldBe` MRConnected
|
||||
getRelation 1 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 2 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 3 vec `shouldBe` MRIntroduced
|
||||
|
||||
it "sets multiple relations 2" $ do
|
||||
let updates = [(0, MRIntroduced), (5, MRConnected), (10, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (5, MRSubjectConnected), (10, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
B.length vec `shouldBe` 11
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 5 vec `shouldBe` MRConnected
|
||||
getRelation 5 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 10 vec `shouldBe` MRIntroduced
|
||||
getRelation 7 vec `shouldBe` MRNew -- Unset position between
|
||||
|
||||
it "handles sparse updates (few indices in large range)" $ do
|
||||
-- Sparse: 3 updates in large group
|
||||
let updates = [(0, MRIntroduced), (100, MRConnected), (5000, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (100, MRSubjectConnected), (5000, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 100 vec `shouldBe` MRConnected
|
||||
getRelation 100 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 5000 vec `shouldBe` MRIntroduced
|
||||
getRelation 50 vec `shouldBe` MRNew -- Untouched position
|
||||
|
||||
it "handles dense updates (many consecutive indices)" $ do
|
||||
-- Dense: many consecutive updates
|
||||
let updates = [(i, if even i then MRIntroduced else MRConnected) | i <- [0 .. 99]]
|
||||
let updates = [(i, if even i then MRIntroduced else MRSubjectConnected) | i <- [0 .. 99]]
|
||||
let vec = setRelations updates B.empty
|
||||
all (\i -> getRelation i vec == (if even i then MRIntroduced else MRConnected)) [0 .. 99] `shouldBe` True
|
||||
all (\i -> getRelation i vec == (if even i then MRIntroduced else MRSubjectConnected)) [0 .. 99] `shouldBe` True
|
||||
|
||||
it "handles unsorted input correctly" $ do
|
||||
let updates = [(10, MRConnected), (2, MRIntroduced), (5, MRConnected), (0, MRIntroduced)]
|
||||
let updates = [(10, MRSubjectConnected), (2, MRIntroduced), (5, MRSubjectConnected), (0, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 2 vec `shouldBe` MRIntroduced
|
||||
getRelation 5 vec `shouldBe` MRConnected
|
||||
getRelation 10 vec `shouldBe` MRConnected
|
||||
getRelation 5 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 10 vec `shouldBe` MRSubjectConnected
|
||||
|
||||
it "handles duplicate indices (last one wins)" $ do
|
||||
let updates = [(0, MRIntroduced), (0, MRConnected), (0, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (0, MRSubjectConnected), (0, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
|
||||
it "preserves existing relations not in update list" $ do
|
||||
let vec1 = setRelation 0 MRConnected B.empty
|
||||
let vec1 = setRelation 0 MRSubjectConnected B.empty
|
||||
let vec2 = setRelation 5 MRIntroduced vec1
|
||||
let updates = [(10, MRConnected)]
|
||||
let updates = [(10, MRSubjectConnected)]
|
||||
let vec3 = setRelations updates vec2
|
||||
getRelation 0 vec3 `shouldBe` MRConnected
|
||||
getRelation 0 vec3 `shouldBe` MRSubjectConnected
|
||||
getRelation 5 vec3 `shouldBe` MRIntroduced
|
||||
getRelation 10 vec3 `shouldBe` MRConnected
|
||||
getRelation 10 vec3 `shouldBe` MRSubjectConnected
|
||||
|
||||
describe "setNewRelations" $ do
|
||||
it "sets multiple new relations with direction" $ do
|
||||
let updates = [(0, (IDSubjectIntroduced, MRIntroduced)), (1, (IDReferencedIntroduced, MRSubjectConnected))]
|
||||
let vec = setNewRelations updates B.empty
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 1 vec `shouldBe` MRSubjectConnected
|
||||
B.unpack vec `shouldBe` [0x01, 0x0A] -- [dir=0,status=001], [dir=1,status=010]
|
||||
|
||||
describe "edge cases and invariants" $ do
|
||||
it "round-trip: set then get returns same value" $ do
|
||||
let vec1 = setRelation 42 MRConnected B.empty
|
||||
getRelation 42 vec1 `shouldBe` MRConnected
|
||||
let vec1 = setRelation 42 MRSubjectConnected B.empty
|
||||
getRelation 42 vec1 `shouldBe` MRSubjectConnected
|
||||
|
||||
it "multiple round-trips preserve values" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelation 1 MRConnected vec1
|
||||
let vec3 = setRelation 2 MRIntroduced vec2
|
||||
getRelation 0 vec3 `shouldBe` MRIntroduced
|
||||
getRelation 1 vec3 `shouldBe` MRConnected
|
||||
getRelation 2 vec3 `shouldBe` MRIntroduced
|
||||
let vec2 = setRelation 1 MRSubjectConnected vec1
|
||||
let vec3 = setRelation 2 MRReferencedConnected vec2
|
||||
let vec4 = setRelation 3 MRConnected vec3
|
||||
getRelation 0 vec4 `shouldBe` MRIntroduced
|
||||
getRelation 1 vec4 `shouldBe` MRSubjectConnected
|
||||
getRelation 2 vec4 `shouldBe` MRReferencedConnected
|
||||
getRelation 3 vec4 `shouldBe` MRConnected
|
||||
|
||||
it "setRelations equivalent to multiple setRelation calls" $ do
|
||||
let updates = [(0, MRIntroduced), (5, MRConnected), (10, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (5, MRSubjectConnected), (10, MRConnected)]
|
||||
let vecBatch = setRelations updates B.empty
|
||||
let vecSeq = setRelation 10 MRIntroduced $ setRelation 5 MRConnected $ setRelation 0 MRIntroduced B.empty
|
||||
let vecSeq = setRelation 10 MRConnected $ setRelation 5 MRSubjectConnected $ setRelation 0 MRIntroduced B.empty
|
||||
vecBatch `shouldBe` vecSeq
|
||||
getRelation 0 vecBatch `shouldBe` getRelation 0 vecSeq
|
||||
getRelation 5 vecBatch `shouldBe` getRelation 5 vecSeq
|
||||
getRelation 10 vecBatch `shouldBe` getRelation 10 vecSeq
|
||||
|
||||
it "handles large group size (10000 members)" $ do
|
||||
let updates = [(0, MRIntroduced), (5000, MRConnected), (9999, MRIntroduced)]
|
||||
let updates = [(0, MRIntroduced), (5000, MRSubjectConnected), (9999, MRIntroduced)]
|
||||
let vec = setRelations updates B.empty
|
||||
B.length vec `shouldBe` 10000
|
||||
getRelation 0 vec `shouldBe` MRIntroduced
|
||||
getRelation 5000 vec `shouldBe` MRConnected
|
||||
getRelation 5000 vec `shouldBe` MRSubjectConnected
|
||||
getRelation 9999 vec `shouldBe` MRIntroduced
|
||||
|
||||
it "all status values can be stored and retrieved" $ do
|
||||
let vec1 = setRelation 0 MRNew B.empty
|
||||
let vec2 = setRelation 1 MRIntroduced vec1
|
||||
let vec3 = setRelation 2 MRConnected vec2
|
||||
getRelation 0 vec3 `shouldBe` MRNew
|
||||
getRelation 1 vec3 `shouldBe` MRIntroduced
|
||||
getRelation 2 vec3 `shouldBe` MRConnected
|
||||
let vec3 = setRelation 2 MRSubjectConnected vec2
|
||||
let vec4 = setRelation 3 MRReferencedConnected vec3
|
||||
let vec5 = setRelation 4 MRConnected vec4
|
||||
getRelation 0 vec5 `shouldBe` MRNew
|
||||
getRelation 1 vec5 `shouldBe` MRIntroduced
|
||||
getRelation 2 vec5 `shouldBe` MRSubjectConnected
|
||||
getRelation 3 vec5 `shouldBe` MRReferencedConnected
|
||||
getRelation 4 vec5 `shouldBe` MRConnected
|
||||
|
||||
it "vector length is minimal (lazy expansion)" $ do
|
||||
let vec = setRelation 3 MRConnected B.empty
|
||||
let vec = setRelation 3 MRSubjectConnected B.empty
|
||||
B.length vec `shouldBe` 4
|
||||
|
||||
it "setRelation preserves existing direction" $ do
|
||||
let vec1 = setNewRelation 0 IDReferencedIntroduced MRIntroduced B.empty
|
||||
let vec2 = setRelation 0 MRConnected vec1
|
||||
getRelation 0 vec2 `shouldBe` MRConnected
|
||||
B.unpack vec2 `shouldBe` [0x0C] -- direction=1 preserved, status=100
|
||||
|
||||
describe "setRelationConnected" $ do
|
||||
it "MRSubjectConnected on MRIntroduced -> MRSubjectConnected" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelationConnected 0 MRSubjectConnected vec1
|
||||
getRelation 0 vec2 `shouldBe` MRSubjectConnected
|
||||
|
||||
it "MRReferencedConnected on MRIntroduced -> MRReferencedConnected" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelationConnected 0 MRReferencedConnected vec1
|
||||
getRelation 0 vec2 `shouldBe` MRReferencedConnected
|
||||
|
||||
it "MRSubjectConnected on MRReferencedConnected -> MRConnected (complementary)" $ do
|
||||
let vec1 = setRelation 0 MRReferencedConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRSubjectConnected vec1
|
||||
getRelation 0 vec2 `shouldBe` MRConnected
|
||||
|
||||
it "MRReferencedConnected on MRSubjectConnected -> MRConnected (complementary)" $ do
|
||||
let vec1 = setRelation 0 MRSubjectConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRReferencedConnected vec1
|
||||
getRelation 0 vec2 `shouldBe` MRConnected
|
||||
|
||||
it "MRSubjectConnected on MRSubjectConnected -> no change" $ do
|
||||
let vec1 = setRelation 0 MRSubjectConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRSubjectConnected vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "MRReferencedConnected on MRReferencedConnected -> no change" $ do
|
||||
let vec1 = setRelation 0 MRReferencedConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRReferencedConnected vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "MRSubjectConnected on MRConnected -> no change" $ do
|
||||
let vec1 = setRelation 0 MRConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRSubjectConnected vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "MRReferencedConnected on MRConnected -> no change" $ do
|
||||
let vec1 = setRelation 0 MRConnected B.empty
|
||||
let vec2 = setRelationConnected 0 MRReferencedConnected vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "invalid status (MRConnected) -> no change" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelationConnected 0 MRConnected vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "invalid status (MRNew) -> no change" $ do
|
||||
let vec1 = setRelation 0 MRIntroduced B.empty
|
||||
let vec2 = setRelationConnected 0 MRNew vec1
|
||||
vec2 `shouldBe` vec1
|
||||
|
||||
it "setRelationConnected preserves direction when updating" $ do
|
||||
let vec1 = setNewRelation 0 IDReferencedIntroduced MRIntroduced B.empty
|
||||
let vec2 = setRelationConnected 0 MRSubjectConnected vec1
|
||||
getRelation' 0 vec2 `shouldBe` (IDReferencedIntroduced, MRSubjectConnected)
|
||||
|
||||
+6
-5
@@ -18,6 +18,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T
|
||||
import Database.SQLite.Simple (Query (..))
|
||||
import Simplex.Chat.Options.SQLite (chatDBFunctions)
|
||||
import Simplex.Chat.Store (createChatStore)
|
||||
import qualified Simplex.Chat.Store as Store
|
||||
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
|
||||
@@ -63,7 +64,7 @@ testVerifySchemaDump :: IO ()
|
||||
testVerifySchemaDump = withTmpFiles $ do
|
||||
savedSchema <- ifM (doesFileExist appSchema) (readFile appSchema) (pure "")
|
||||
savedSchema `deepseq` pure ()
|
||||
void $ createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing)
|
||||
void $ createChatStore (DBOpts testDB chatDBFunctions "" False True TQOff) (MigrationConfig MCError Nothing)
|
||||
getSchema testDB appSchema `shouldReturn` savedSchema
|
||||
removeFile testDB
|
||||
|
||||
@@ -71,14 +72,14 @@ testVerifyLintFKeyIndexes :: IO ()
|
||||
testVerifyLintFKeyIndexes = withTmpFiles $ do
|
||||
savedLint <- ifM (doesFileExist appLint) (readFile appLint) (pure "")
|
||||
savedLint `deepseq` pure ()
|
||||
void $ createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing)
|
||||
void $ createChatStore (DBOpts testDB chatDBFunctions "" False True TQOff) (MigrationConfig MCError Nothing)
|
||||
getLintFKeyIndexes testDB "tests/tmp/chat_lint.sql" `shouldReturn` savedLint
|
||||
removeFile testDB
|
||||
|
||||
testSchemaMigrations :: IO ()
|
||||
testSchemaMigrations = withTmpFiles $ do
|
||||
let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) Store.migrations
|
||||
Right st <- createDBStore (DBOpts testDB "" False True TQOff) noDownMigrations (MigrationConfig MCError Nothing)
|
||||
Right st <- createDBStore (DBOpts testDB chatDBFunctions "" False True TQOff) noDownMigrations (MigrationConfig MCError Nothing)
|
||||
mapM_ (testDownMigration st) $ drop (length noDownMigrations) Store.migrations
|
||||
closeDBStore st
|
||||
removeFile testDB
|
||||
@@ -152,7 +153,7 @@ saveQueryPlans = it "verify and overwrite query plans" $ \TestParams {chatQueryS
|
||||
updatePlans
|
||||
appChatQueryPlans
|
||||
chatQueryStats
|
||||
(createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing))
|
||||
(createChatStore (DBOpts testDB chatDBFunctions "" False True TQOff) (MigrationConfig MCError Nothing))
|
||||
(\db -> do
|
||||
DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_conn_ids (conn_id BLOB)"
|
||||
DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)"
|
||||
@@ -161,7 +162,7 @@ saveQueryPlans = it "verify and overwrite query plans" $ \TestParams {chatQueryS
|
||||
updatePlans
|
||||
appAgentQueryPlans
|
||||
agentQueryStats
|
||||
(createAgentStore (DBOpts testAgentDB "" False True TQOff) (MigrationConfig MCError Nothing))
|
||||
(createAgentStore (DBOpts testAgentDB [] "" False True TQOff) (MigrationConfig MCError Nothing))
|
||||
(const $ pure ())
|
||||
chatSavedPlans' == chatSavedPlans `shouldBe` True
|
||||
agentSavedPlans' == agentSavedPlans `shouldBe` True
|
||||
|
||||
Reference in New Issue
Block a user