directory: move all storage to the database, optimize performance (#6287)

* core: add custom indexed columns to groups and contacts

* directory: use database (TODO search/listing)

* triggers to maintain current member count

* update simplexmq, fix tests, use summary from GroupInfo

* fix all directory tests

* remove acceptance fields from group reg

* enable all tests

* clean up

* postgres migrations, fixes

* query plans

* use function in postgres triggers, improve sqlite query

* fix export/import

* update schema

* prevent admins from promoting groups when approving

* update listing every 5 minutes
This commit is contained in:
Evgeny
2025-09-28 15:11:49 +01:00
committed by GitHub
parent 53d6d057c6
commit 015d5de364
40 changed files with 1897 additions and 1054 deletions
+15 -5
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main where
@@ -5,12 +6,21 @@ module Main where
import Directory.Options
import Directory.Service
import Directory.Store
import Directory.Store.Migrate
import Simplex.Chat.Terminal (terminalChatConfig)
main :: IO ()
main = do
opts@DirectoryOpts {directoryLog, runCLI} <- welcomeGetOpts
st <- restoreDirectoryStore directoryLog
if runCLI
then directoryServiceCLI st opts
else directoryService st opts terminalChatConfig
opts@DirectoryOpts {directoryLog, migrateDirectoryLog, runCLI} <- welcomeGetOpts
case migrateDirectoryLog of
Just cmd -> migrate cmd opts terminalChatConfig
Nothing -> do
st <- openDirectoryLog directoryLog
if runCLI
then directoryServiceCLI st opts
else directoryService st opts terminalChatConfig
where
migrate = \case
MLCheck -> checkDirectoryLog
MLImport -> importDirectoryLogToDB
MLExport -> exportDBToDirectoryLog
@@ -11,7 +11,6 @@
module Directory.Listing where
import Control.Applicative ((<|>))
import Control.Concurrent.STM
import Control.Monad
import Crypto.Hash (Digest, MD5)
import qualified Crypto.Hash as CH
@@ -26,7 +25,6 @@ import qualified Data.ByteString.Lazy as LB
import Data.Int (Int64)
import Data.List (isPrefixOf)
import Data.Maybe (catMaybes, fromMaybe)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -90,10 +88,10 @@ recentRoundedTime roundTo now t
let secs = (systemSeconds (utcToSystemTime t) `div` roundTo) * roundTo
in Just $ systemToUTCTime $ MkSystemTime secs 0
groupDirectoryEntry :: UTCTime -> GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData))
groupDirectoryEntry now (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) =
groupDirectoryEntry :: UTCTime -> GroupInfo -> Maybe GroupLink -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData))
groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ =
let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile
entryType = DETGroup memberAdmission summary
entryType = DETGroup memberAdmission groupSummary
entry groupLink =
let de =
DirectoryEntry
@@ -122,22 +120,21 @@ groupDirectoryEntry now (GIS GroupInfo {groupProfile, chatTs, createdAt} summary
Right img'' -> Just (imgFile, img'')
Left _ -> Nothing
generateListing :: DirectoryStore -> FilePath -> [GroupInfoSummary] -> IO ()
generateListing st dir gs = do
generateListing :: FilePath -> [(GroupInfo, GroupReg, Maybe GroupLink)] -> IO ()
generateListing dir gs = do
createDirectoryIfMissing True dir
oldDirs <- filter ((directoryDataPath <> ".") `isPrefixOf`) <$> listDirectory dir
ts <- getCurrentTime
let newDirPath = directoryDataPath <> "." <> iso8601Show ts <> "/"
newDir = dir </> newDirPath
gs' <- filterListedGroups st gs
createDirectoryIfMissing True (newDir </> listingImageFolder)
gs'' <-
fmap catMaybes $ forM gs' $ \g@(GIS GroupInfo {groupId} _ _) ->
forM (groupDirectoryEntry ts g) $ \(g', img) -> do
gs' <-
fmap catMaybes $ forM gs $ \(g, gr, link_) ->
forM (groupDirectoryEntry ts g link_) $ \(g', img) -> do
forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir </> imgFile) imgData
pure (groupId, g')
saveListing newDir listingFileName gs''
saveListing newDir promotedFileName =<< filterPromotedGroups st gs''
pure (g', gr)
saveListing newDir listingFileName gs'
saveListing newDir promotedFileName $ filter (\(_, GroupReg {promoted}) -> promoted) gs'
-- atomically update the link
let newSymLink = newDir <> ".link"
symLink = dir </> directoryDataPath
@@ -145,12 +142,7 @@ generateListing st dir gs = do
renamePath newSymLink symLink
mapM_ (removePathForcibly . (dir </>)) oldDirs
where
saveListing newDir f = LB.writeFile (newDir </> f) . J.encode . DirectoryListing . map snd
filterPromotedGroups :: DirectoryStore -> [(GroupId, DirectoryEntry)] -> IO [(GroupId, DirectoryEntry)]
filterPromotedGroups st gs = do
pgs <- readTVarIO $ promotedGroups st
pure $ filter (\g -> fst g `S.member` pgs) gs
saveListing newDir f = LB.writeFile (newDir </> f) . J.encode . DirectoryListing . map fst
toFormattedText :: Text -> MarkdownList
toFormattedText t = fromMaybe [FormattedText Nothing t] $ parseMaybeMarkdownList t
@@ -7,16 +7,20 @@
module Directory.Options
( DirectoryOpts (..),
MigrateLog (..),
getDirectoryOpts,
mkChatOpts,
)
where
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Options.Applicative
import Simplex.Chat.Bot.KnownContacts
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP)
import Simplex.Messaging.Parsers (parseAll)
data DirectoryOpts = DirectoryOpts
{ coreOptions :: CoreChatOpts,
@@ -30,6 +34,7 @@ data DirectoryOpts = DirectoryOpts
profileNameLimit :: Int,
captchaGenerator :: Maybe FilePath,
directoryLog :: Maybe FilePath,
migrateDirectoryLog :: Maybe MigrateLog,
serviceName :: T.Text,
runCLI :: Bool,
searchResults :: Int,
@@ -37,6 +42,8 @@ data DirectoryOpts = DirectoryOpts
testing :: Bool
}
data MigrateLog = MLCheck | MLImport | MLExport
directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts
directoryOpts appDir defaultDbName = do
coreOptions <- coreChatOptsP appDir defaultDbName
@@ -107,12 +114,20 @@ directoryOpts appDir defaultDbName = do
<> help "Executable to generate captcha files, must accept text as parameter and save file to stdout as base64 up to 12500 bytes"
)
directoryLog <-
Just
<$> strOption
optional $
strOption
( long "directory-file"
<> metavar "DIRECTORY_FILE"
<> help "Append only log for directory state"
)
migrateDirectoryLog <-
optional $
option
parseMigrateLog
( long "migrate-directory-file"
<> metavar "MIGRATE_COMMAND"
<> help "Command to import/export directory log file"
)
serviceName <-
strOption
( long "service-name"
@@ -145,6 +160,7 @@ directoryOpts appDir defaultDbName = do
profileNameLimit,
captchaGenerator,
directoryLog,
migrateDirectoryLog,
serviceName = T.pack serviceName,
runCLI,
searchResults = 10,
@@ -181,3 +197,13 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName} =
createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False},
maintenance = False
}
parseMigrateLog :: ReadM MigrateLog
parseMigrateLog = eitherReader $ parseAll mlP . encodeUtf8 . T.pack
where
mlP =
A.takeTill (== ' ') >>= \case
"check" -> pure MLCheck
"import" -> pure MLImport
"export" -> pure MLExport
_ -> fail "bad MigrateLog"
@@ -1,12 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Directory.Search where
import Data.List (sortOn)
import Data.Ord (Down (..))
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Types
@@ -14,19 +7,7 @@ import Simplex.Chat.Types
data SearchRequest = SearchRequest
{ searchType :: SearchType,
searchTime :: UTCTime,
sentGroups :: Set GroupId
lastGroup :: GroupId -- cursor for search
}
data SearchType = STAll | STRecent | STSearch Text
takeTop :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers} _) -> Down currentMembers)
takeRecent :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _ _) -> Down createdAt)
groupIds :: [GroupInfoSummary] -> Set GroupId
groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _ _) -> groupId)
filterNotSent :: Set GroupId -> [GroupInfoSummary] -> [GroupInfoSummary]
filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.notMember` sentGroups)
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,17 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Directory.Store
( DirectoryStore (..),
( DirectoryLog (..),
GroupReg (..),
GroupRegStatus (..),
UserGroupRegId,
@@ -15,17 +20,28 @@ module Directory.Store
DirectoryMemberAcceptance (..),
DirectoryStatus (..),
ProfileCondition (..),
restoreDirectoryStore,
addGroupReg,
DirectoryLogRecord (..),
openDirectoryLog,
readDirectoryLogData,
addGroupRegStore,
insertGroupReg,
delGroupReg,
deleteGroupReg,
setGroupStatusStore,
setGroupStatusPromoStore,
setGroupPromotedStore,
grDirectoryStatus,
setGroupRegOwner,
getGroupReg,
getUserGroupReg,
getUserGroupRegs,
filterListedGroups,
getAllGroupRegs_,
getDuplicateGroupRegs,
getGroupReg,
getGroupAndReg,
listLastGroups,
listPendingGroups,
getAllListedGroups,
searchListedGroups,
groupRegStatusText,
pendingApproval,
groupRemoved,
@@ -35,12 +51,19 @@ module Directory.Store
basicJoinFilter,
moderateJoinFilter,
strongJoinFilter,
groupDBError,
logGCreate,
logGDelete,
logGUpdateOwner,
logGUpdateStatus,
logGUpdatePromotion,
)
where
import Control.Applicative ((<|>))
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Data.Aeson ((.:), (.=))
import qualified Data.Aeson.KeyMap as JM
import qualified Data.Aeson.TH as JQ
@@ -49,51 +72,51 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Int (Int64)
import Data.List (find, foldl', sortOn)
import Data.List (sortOn)
import Data.Map (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock (UTCTime (..), getCurrentTime)
import Data.Time.Clock.System (systemEpochDay)
import Directory.Search
import Directory.Util
import Simplex.Chat.Controller
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Store
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Shared (groupInfoQueryFields, groupInfoQueryFrom)
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Store.DB (BoolInt (..), fromTextField_)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
import Simplex.Messaging.Util (ifM, whenM)
import System.Directory
import Simplex.Messaging.Util (eitherToMaybe, firstRow, maybeFirstRow', safeDecodeUtf8)
import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile)
data DirectoryStore = DirectoryStore
{ groupRegs :: TVar [GroupReg], -- most recent first, reversed when listed
listedGroups :: TVar (Set GroupId), -- includes promoted
promotedGroups :: TVar (Set GroupId),
reservedGroups :: TVar (Set GroupId),
directoryLogFile :: Maybe Handle
}
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
import Database.PostgreSQL.Simple.SqlQQ (sql)
#else
import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql)
#endif
data DirectoryStoreData = DirectoryStoreData
{ groupRegs_ :: [GroupReg],
listedGroups_ :: Set GroupId,
promotedGroups_ :: Set GroupId,
reservedGroups_ :: Set GroupId
data DirectoryLog = DirectoryLog
{ directoryLogFile :: Maybe Handle
}
data GroupReg = GroupReg
{ dbGroupId :: GroupId,
userGroupRegId :: UserGroupRegId,
dbContactId :: ContactId,
dbOwnerMemberId :: TVar (Maybe GroupMemberId),
groupRegStatus :: TVar GroupRegStatus,
promoted :: TVar Bool
}
data GroupRegData = GroupRegData
{ dbGroupId_ :: GroupId,
userGroupRegId_ :: UserGroupRegId,
dbContactId_ :: ContactId,
dbOwnerMemberId_ :: Maybe GroupMemberId,
groupRegStatus_ :: GroupRegStatus,
promoted_ :: Bool
dbOwnerMemberId :: Maybe GroupMemberId,
groupRegStatus :: GroupRegStatus,
promoted :: Bool,
createdAt :: UTCTime
}
data DirectoryGroupData = DirectoryGroupData
@@ -203,101 +226,219 @@ toCustomData :: DirectoryGroupData -> CustomData
toCustomData DirectoryGroupData {memberAcceptance} =
CustomData $ JM.fromList ["memberAcceptance" .= memberAcceptance]
addGroupReg :: DirectoryStore -> Contact -> GroupInfo -> GroupRegStatus -> IO UserGroupRegId
addGroupReg st ct GroupInfo {groupId} grStatus = do
grData <- addGroupReg_
logGCreate st grData
pure $ userGroupRegId_ grData
addGroupRegStore :: ChatController -> Contact -> GroupInfo -> GroupRegStatus -> IO (Either String GroupReg)
addGroupRegStore cc Contact {contactId = dbContactId} GroupInfo {groupId = dbGroupId} groupRegStatus =
withDB' "addGroupRegStore" cc $ \db -> do
createdAt <- getCurrentTime
maxUgrId <-
maybeFirstRow' 0 (fromMaybe 0 . fromOnly) $
DB.query db "SELECT MAX(user_group_reg_id) FROM sx_directory_group_regs WHERE contact_id = ?" (Only dbContactId)
let gr = GroupReg {dbGroupId, userGroupRegId = maxUgrId + 1, dbContactId, dbOwnerMemberId = Nothing, groupRegStatus, promoted = False, createdAt}
insertGroupReg db gr
pure gr
insertGroupReg :: DB.Connection -> GroupReg -> IO ()
insertGroupReg db GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt} = do
DB.execute
db
[sql|
INSERT INTO sx_directory_group_regs
(group_id, user_group_reg_id, contact_id, owner_member_id, group_reg_status, group_promoted, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?)
|]
(dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, BI promoted, createdAt, createdAt)
delGroupReg :: ChatController -> GroupId -> IO (Either String ())
delGroupReg cc gId = withDB' "delGroupReg" cc (`deleteGroupReg` gId)
deleteGroupReg :: DB.Connection -> GroupId -> IO ()
deleteGroupReg db gId = DB.execute db "DELETE FROM sx_directory_group_regs WHERE group_id = ?" (Only gId)
setGroupStatusStore :: ChatController -> GroupId -> GroupRegStatus -> IO (Either String (GroupRegStatus, GroupReg))
setGroupStatusStore cc gId grStatus' =
withDB "setGroupStatusStore" cc $ \db -> do
gr <- getGroupReg_ db gId
ts <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_reg_status = ?, updated_at = ? WHERE group_id = ?" (grStatus', ts, gId)
pure (groupRegStatus gr, gr {groupRegStatus = grStatus'})
setGroupStatusPromoStore :: ChatController -> GroupId -> GroupRegStatus -> Bool -> IO (Either String (DirectoryStatus, Bool))
setGroupStatusPromoStore cc gId grStatus' grPromoted' =
withDB "setGroupStatusPromoStore" cc $ \db -> do
GroupReg {groupRegStatus, promoted} <- getGroupReg_ db gId
ts <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_reg_status = ?, group_promoted = ?, updated_at = ? WHERE group_id = ?" (grStatus', BI grPromoted', ts, gId)
pure (grDirectoryStatus groupRegStatus, promoted)
setGroupPromotedStore :: ChatController -> GroupId -> Bool -> IO (Either String (DirectoryStatus, Bool))
setGroupPromotedStore cc gId grPromoted' =
withDB "setGroupPromotedStore" cc $ \db -> do
GroupReg {groupRegStatus, promoted} <- getGroupReg_ db gId
ts <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_promoted = ?, updated_at = ? WHERE group_id = ?" (BI grPromoted', ts, gId)
pure (grDirectoryStatus groupRegStatus, promoted)
groupDBError :: StoreError -> String
groupDBError = \case
SEGroupNotFound _ -> "group not found"
e -> show e
setGroupRegOwner :: ChatController -> GroupId -> GroupMember -> IO (Either String ())
setGroupRegOwner cc gId owner = do
ts <- getCurrentTime
withDB' "setGroupRegOwner" cc $ \db ->
DB.execute
db
[sql|
UPDATE sx_directory_group_regs
SET owner_member_id = ?, updated_at = ?
WHERE group_id = ?
|]
(groupMemberId' owner, ts, gId)
getGroupReg :: ChatController -> GroupId -> IO (Either String GroupReg)
getGroupReg cc gId = withDB "getGroupReg" cc (`getGroupReg_` gId)
getGroupReg_ :: DB.Connection -> GroupId -> ExceptT String IO GroupReg
getGroupReg_ db gId =
ExceptT $ firstRow rowToGroupReg "group registration not found" $
DB.query
db
[sql|
SELECT group_id, user_group_reg_id, contact_id, owner_member_id, group_reg_status, group_promoted, created_at
FROM sx_directory_group_regs
WHERE group_id = ?
|]
(Only gId)
getGroupAndReg :: ChatController -> User -> GroupId -> IO (Either String (GroupInfo, GroupReg))
getGroupAndReg cc user@User {userId, userContactId} gId =
withDB "getGroupAndReg" cc $ \db ->
ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show gId ++ " not found") $
DB.query db (groupReqQuery <> " AND g.group_id = ?") (userId, userContactId, gId)
getUserGroupReg :: ChatController -> User -> ContactId -> UserGroupRegId -> IO (Either String (GroupInfo, GroupReg))
getUserGroupReg cc user@User {userId, userContactId} ctId ugrId =
withDB "getUserGroupReg" cc $ \db ->
ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show ugrId ++ " not found") $
DB.query db (groupReqQuery <> " AND r.contact_id = ? AND r.user_group_reg_id = ?") (userId, userContactId, ctId, ugrId)
getUserGroupRegs :: ChatController -> User -> ContactId -> IO (Either String [(GroupInfo, GroupReg)])
getUserGroupRegs cc user@User {userId, userContactId} ctId =
withDB' "getUserGroupRegs" cc $ \db ->
map (toGroupInfoReg (vr cc) user)
<$> DB.query db (groupReqQuery <> " AND r.contact_id = ? ORDER BY r.user_group_reg_id") (userId, userContactId, ctId)
getAllListedGroups :: ChatController -> User -> IO (Either String [(GroupInfo, GroupReg, Maybe GroupLink)])
getAllListedGroups cc user@User {userId, userContactId} =
withDB' "getAllListedGroups" cc $ \db ->
DB.query db (groupReqQuery <> " AND r.group_reg_status = ?") (userId, userContactId, GRSActive)
>>= mapM (withGroupLink db . toGroupInfoReg (vr cc) user)
where
addGroupReg_ = do
let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus, promoted_ = False}
gr <- dataToGroupReg grData
atomically $ stateTVar (groupRegs st) $ \grs ->
let ugrId = 1 + foldl' maxUgrId 0 grs
grData' = grData {userGroupRegId_ = ugrId}
gr' = gr {userGroupRegId = ugrId}
in (grData', gr' : grs)
ctId = contactId' ct
maxUgrId mx GroupReg {dbContactId, userGroupRegId}
| dbContactId == ctId && userGroupRegId > mx = userGroupRegId
| otherwise = mx
withGroupLink db (g, gr) = (g,gr,) . eitherToMaybe <$> runExceptT (getGroupLink db user g)
delGroupReg :: DirectoryStore -> GroupReg -> IO ()
delGroupReg st gr@GroupReg {dbGroupId = gId, groupRegStatus} = do
logGDelete st gId
atomically $ writeTVar groupRegStatus GRSRemoved
atomically $ unlistGroup st gr
atomically $ modifyTVar' (groupRegs st) $ filter ((gId /=) . dbGroupId)
setGroupStatusStore :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO DirectoryStatus
setGroupStatusStore st gr grStatus' = do
logGUpdateStatus st (dbGroupId gr) grStatus'
atomically $ do
grStatus <- swapTVar (groupRegStatus gr) grStatus'
updateListing st gr
pure $ grDirectoryStatus grStatus
searchListedGroups :: ChatController -> User -> SearchType -> Maybe GroupId -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pageSize =
withDB' "searchListedGroups" cc $ \db ->
case searchType of
STAll -> case lastGroup_ of
Nothing -> do
gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
n <- count $ DB.query db countQuery' (Only GRSActive)
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId)
pure (gs, n)
where
countQuery' = countQuery <> " WHERE r.group_reg_status = ? "
orderBy = " ORDER BY g.summary_current_members_count DESC "
STRecent -> case lastGroup_ of
Nothing -> do
gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
n <- count $ DB.query db countQuery' (Only GRSActive)
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId)
pure (gs, n)
where
countQuery' = countQuery <> " WHERE r.group_reg_status = ? "
orderBy = " ORDER BY r.created_at DESC "
STSearch search -> case lastGroup_ of
Nothing -> do
gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize)
n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s)
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s)
pure (gs, n)
where
s = T.toLower search
countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? "
orderBy = " ORDER BY g.summary_current_members_count DESC "
where
status' = grDirectoryStatus grStatus'
updateListing = case status' of
DSListed -> listGroup
DSReserved -> reserveGroup
DSRegistered -> unlistGroup
DSRemoved -> unlistGroup
groups = (map (toGroupInfoReg (vr cc) user) <$>)
count = maybeFirstRow' 0 fromOnly
listedGroupQuery = groupReqQuery <> " AND r.group_reg_status = ? "
countQuery = "SELECT COUNT(1) FROM groups g JOIN sx_directory_group_regs r ON g.group_id = r.group_id "
searchCond =
[sql|
AND (LOWER(gp.display_name) LIKE '%' || ? || '%'
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
OR LOWER(gp.description) LIKE '%' || ? || '%'
)
|]
setGroupPromotedStore :: DirectoryStore -> GroupReg -> Bool -> IO (DirectoryStatus, Bool)
setGroupPromotedStore st gr grPromoted' = do
let gId = dbGroupId gr
logGUpdatePromotion st gId grPromoted'
atomically $ do
grPromoted <- swapTVar (promoted gr) grPromoted'
status <- grDirectoryStatus <$> readTVar (groupRegStatus gr)
let update = if status == DSListed && grPromoted' then S.insert else S.delete
modifyTVar' (promotedGroups st) $ update gId
pure (status, grPromoted)
getAllGroupRegs_ :: DB.Connection -> User -> IO [(GroupInfo, GroupReg)]
getAllGroupRegs_ db user@User {userId, userContactId} =
map (toGroupInfoReg supportedChatVRange user)
<$> DB.query db groupReqQuery (userId, userContactId)
setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO ()
setGroupRegOwner st gr owner = do
let memberId = groupMemberId' owner
logGUpdateOwner st (dbGroupId gr) memberId
atomically $ writeTVar (dbOwnerMemberId gr) (Just memberId)
getDuplicateGroupRegs :: ChatController -> User -> Text -> IO (Either String [(GroupInfo, GroupReg)])
getDuplicateGroupRegs cc user@User {userId, userContactId} displayName =
withDB' "getDuplicateGroupRegs" cc $ \db ->
map (toGroupInfoReg (vr cc) user)
<$> DB.query db (groupReqQuery <> " AND gp.display_name = ?") (userId, userContactId, displayName)
getGroupReg :: DirectoryStore -> GroupId -> IO (Maybe GroupReg)
getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVarIO (groupRegs st)
listLastGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
listLastGroups cc user@User {userId, userContactId} count =
withDB' "getUserGroupRegs" cc $ \db -> do
gs <-
map (toGroupInfoReg (vr cc) user)
<$> DB.query db (groupReqQuery <> " ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count)
n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs"
pure (gs, n)
getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> IO (Maybe GroupReg)
getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVarIO (groupRegs st)
listPendingGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
listPendingGroups cc user@User {userId, userContactId} count =
withDB' "getUserGroupRegs" cc $ \db -> do
gs <-
map (toGroupInfoReg (vr cc) user)
<$> DB.query db (groupReqQuery <> " AND r.group_reg_status LIKE 'pending_approval%' ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count)
n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs WHERE group_reg_status LIKE 'pending_approval%'"
pure (gs, n)
getUserGroupRegs :: DirectoryStore -> ContactId -> IO [GroupReg]
getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (groupRegs st)
toGroupInfoReg :: VersionRangeChat -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg)
toGroupInfoReg vr' User {userContactId} (groupRow :. grRow) =
(toGroupInfo vr' userContactId [] groupRow, rowToGroupReg grRow)
filterListedGroups :: DirectoryStore -> [GroupInfoSummary] -> IO [GroupInfoSummary]
filterListedGroups st gs = do
lgs <- readTVarIO $ listedGroups st
pure $ filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.member` lgs) gs
type GroupRegRow = (GroupId, UserGroupRegId, ContactId, Maybe GroupMemberId, GroupRegStatus, BoolInt, UTCTime)
listGroup :: DirectoryStore -> GroupReg -> STM ()
listGroup st gr = do
let gId = dbGroupId gr
modifyTVar' (listedGroups st) $ S.insert gId
whenM (readTVar $ promoted gr) $ modifyTVar' (promotedGroups st) $ S.insert gId
modifyTVar' (reservedGroups st) $ S.delete gId
rowToGroupReg :: GroupRegRow -> GroupReg
rowToGroupReg (dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, BI promoted, createdAt) =
GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt}
reserveGroup :: DirectoryStore -> GroupReg -> STM ()
reserveGroup st gr = do
let gId = dbGroupId gr
modifyTVar' (listedGroups st) $ S.delete gId
modifyTVar' (promotedGroups st) $ S.delete gId
modifyTVar' (reservedGroups st) $ S.insert gId
unlistGroup :: DirectoryStore -> GroupReg -> STM ()
unlistGroup st gr = do
let gId = dbGroupId gr
modifyTVar' (listedGroups st) $ S.delete gId
modifyTVar' (promotedGroups st) $ S.delete gId
modifyTVar' (reservedGroups st) $ S.delete gId
groupReqQuery :: Query
groupReqQuery = groupInfoQueryFields <> groupRegFields <> groupInfoQueryFrom <> groupRegFromCond
where
groupRegFields = ", r.group_id, r.user_group_reg_id, r.contact_id, r.owner_member_id, r.group_reg_status, r.group_promoted, r.created_at "
groupRegFromCond = " JOIN sx_directory_group_regs r ON r.group_id = g.group_id WHERE g.user_id = ? AND mu.contact_id = ? "
data DirectoryLogRecord
= GRCreate GroupRegData
= GRCreate GroupReg
| GRDelete GroupId
| GRUpdateStatus GroupId GroupRegStatus
| GRUpdatePromotion GroupId Bool
@@ -310,22 +451,22 @@ data DLRTag
| GRUpdatePromotion_
| GRUpdateOwner_
logDLR :: DirectoryStore -> DirectoryLogRecord -> IO ()
logDLR :: DirectoryLog -> DirectoryLogRecord -> IO ()
logDLR st r = forM_ (directoryLogFile st) $ \h -> B.hPutStrLn h (strEncode r)
logGCreate :: DirectoryStore -> GroupRegData -> IO ()
logGCreate :: DirectoryLog -> GroupReg -> IO ()
logGCreate st = logDLR st . GRCreate
logGDelete :: DirectoryStore -> GroupId -> IO ()
logGDelete :: DirectoryLog -> GroupId -> IO ()
logGDelete st = logDLR st . GRDelete
logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO ()
logGUpdateStatus :: DirectoryLog -> GroupId -> GroupRegStatus -> IO ()
logGUpdateStatus st gId = logDLR st . GRUpdateStatus gId
logGUpdatePromotion :: DirectoryStore -> GroupId -> Bool -> IO ()
logGUpdatePromotion :: DirectoryLog -> GroupId -> Bool -> IO ()
logGUpdatePromotion st gId = logDLR st . GRUpdatePromotion gId
logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO ()
logGUpdateOwner :: DirectoryLog -> GroupId -> GroupMemberId -> IO ()
logGUpdateOwner st gId = logDLR st . GRUpdateOwner gId
instance StrEncoding DLRTag where
@@ -359,24 +500,25 @@ instance StrEncoding DirectoryLogRecord where
GRUpdatePromotion_ -> GRUpdatePromotion <$> A.decimal <*> _strP
GRUpdateOwner_ -> GRUpdateOwner <$> A.decimal <* A.space <*> A.decimal
instance StrEncoding GroupRegData where
strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_} =
instance StrEncoding GroupReg where
strEncode GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted} =
B.unwords $
[ "group_id=" <> strEncode dbGroupId_,
"user_group_id=" <> strEncode userGroupRegId_,
"contact_id=" <> strEncode dbContactId_,
"owner_member_id=" <> strEncode dbOwnerMemberId_,
"status=" <> strEncode groupRegStatus_
[ "group_id=" <> strEncode dbGroupId,
"user_group_id=" <> strEncode userGroupRegId,
"contact_id=" <> strEncode dbContactId,
"owner_member_id=" <> strEncode dbOwnerMemberId,
"status=" <> strEncode groupRegStatus
]
<> ["promoted=" <> strEncode promoted_ | promoted_]
<> ["promoted=" <> strEncode promoted | promoted]
strP = do
dbGroupId_ <- "group_id=" *> strP_
userGroupRegId_ <- "user_group_id=" *> strP_
dbContactId_ <- "contact_id=" *> strP_
dbOwnerMemberId_ <- "owner_member_id=" *> strP_
groupRegStatus_ <- "status=" *> strP
promoted_ <- (" promoted=" *> strP) <|> pure False
pure GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_}
dbGroupId <- "group_id=" *> strP_
userGroupRegId <- "user_group_id=" *> strP_
dbContactId <- "contact_id=" *> strP_
dbOwnerMemberId <- "owner_member_id=" *> strP_
groupRegStatus <- "status=" *> strP
promoted <- (" promoted=" *> strP) <|> pure False
let createdAt = UTCTime systemEpochDay 0
pure GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt}
instance StrEncoding GroupRegStatus where
strEncode = \case
@@ -400,78 +542,30 @@ instance StrEncoding GroupRegStatus where
"removed" -> pure GRSRemoved
_ -> fail "invalid GroupRegStatus"
dataToGroupReg :: GroupRegData -> IO GroupReg
dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_} = do
dbOwnerMemberId <- newTVarIO dbOwnerMemberId_
groupRegStatus <- newTVarIO groupRegStatus_
promoted <- newTVarIO promoted_
pure
GroupReg
{ dbGroupId = dbGroupId_,
userGroupRegId = userGroupRegId_,
dbContactId = dbContactId_,
dbOwnerMemberId,
groupRegStatus,
promoted
}
instance ToField GroupRegStatus where toField = toField . safeDecodeUtf8 . strEncode
restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore
restoreDirectoryStore = \case
Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= newDirectoryStore . Just)
Nothing -> newDirectoryStore Nothing
instance FromField GroupRegStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
openDirectoryLog :: Maybe FilePath -> IO DirectoryLog
openDirectoryLog = \case
Just f -> DirectoryLog . Just <$> openLogFile f
Nothing -> pure $ DirectoryLog Nothing
where
newFile f = do
h <- openFile f WriteMode
openLogFile f = do
h <- openFile f AppendMode
hSetBuffering h LineBuffering
pure h
restore f = do
grs <- readDirectoryData f
renameFile f (f <> ".bak")
h <- writeDirectoryData f grs -- compact
mkDirectoryStore h grs
emptyStoreData :: DirectoryStoreData
emptyStoreData = DirectoryStoreData [] S.empty S.empty S.empty
newDirectoryStore :: Maybe Handle -> IO DirectoryStore
newDirectoryStore = (`mkDirectoryStore_` emptyStoreData)
mkDirectoryStore :: Handle -> [GroupRegData] -> IO DirectoryStore
mkDirectoryStore h groups =
foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h)
where
addGroupRegData d gr@GroupRegData {dbGroupId_ = gId} = do
gr' <- dataToGroupReg gr
let !grs' = gr' : groupRegs_ d
pure $ case grDirectoryStatus $ groupRegStatus_ gr of
DSListed ->
let !listed = S.insert gId $ listedGroups_ d
!promoted = (if promoted_ gr then S.insert gId else id) $ promotedGroups_ d
in d {groupRegs_ = grs', listedGroups_ = listed, promotedGroups_ = promoted}
DSReserved ->
let !reserved = S.insert gId $ reservedGroups_ d
in d {groupRegs_ = grs', reservedGroups_ = reserved}
DSRegistered -> d {groupRegs_ = grs'}
DSRemoved -> d
mkDirectoryStore_ :: Maybe Handle -> DirectoryStoreData -> IO DirectoryStore
mkDirectoryStore_ h d = do
groupRegs <- newTVarIO $ groupRegs_ d
listedGroups <- newTVarIO $ listedGroups_ d
promotedGroups <- newTVarIO $ promotedGroups_ d
reservedGroups <- newTVarIO $ reservedGroups_ d
pure DirectoryStore {groupRegs, listedGroups, promotedGroups, reservedGroups, directoryLogFile = h}
readDirectoryData :: FilePath -> IO [GroupRegData]
readDirectoryData f =
sortOn dbGroupId_ . M.elems
readDirectoryLogData :: FilePath -> IO [GroupReg]
readDirectoryLogData f =
sortOn dbGroupId . M.elems
<$> (foldM processDLR M.empty . B.lines =<< B.readFile f)
where
processDLR :: Map GroupId GroupRegData -> ByteString -> IO (Map GroupId GroupRegData)
processDLR :: Map GroupId GroupReg -> ByteString -> IO (Map GroupId GroupReg)
processDLR m l = case strDecode l of
Left e -> m <$ putStrLn ("Error parsing log record: " <> e <> ", " <> B.unpack (B.take 80 l))
Right r -> case r of
GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do
GRCreate gr@GroupReg {dbGroupId = gId} -> do
when (isJust $ M.lookup gId m) $
putStrLn $
"Warning: duplicate group with ID " <> show gId <> ", group replaced."
@@ -479,19 +573,12 @@ readDirectoryData f =
GRDelete gId -> case M.lookup gId m of
Just _ -> pure $ M.delete gId m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", deletion ignored.")
GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {groupRegStatus_} m
GRUpdateStatus gId groupRegStatus -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {groupRegStatus} m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.")
GRUpdatePromotion gId promoted_ -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {promoted_} m
GRUpdatePromotion gId promoted -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {promoted} m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", promotion update ignored.")
GRUpdateOwner gId grOwnerId -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m
Just gr -> pure $ M.insert gId gr {dbOwnerMemberId = Just grOwnerId} m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.")
writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle
writeDirectoryData f grs = do
h <- openFile f WriteMode
hSetBuffering h LineBuffering
forM_ grs $ B.hPutStrLn h . strEncode . GRCreate
pure h
@@ -0,0 +1,115 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Directory.Store.Migrate where
import Control.Monad
import Control.Monad.Except
import qualified Data.ByteString.Char8 as B
import Data.List (find)
import Directory.Options
import Directory.Store
import Simplex.Chat (createChatDatabase)
import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..))
import Simplex.Chat.Options (CoreChatOpts (..))
import Simplex.Chat.Options.DB
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store.Groups (getHostMember)
import Simplex.Chat.Store.Profiles (getUsers)
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Store.Common
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Agent.Store.Interface (closeDBStore, migrateDBSchema)
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (whenM)
import System.Directory (doesFileExist, renamePath)
import System.Exit (exitFailure)
import System.IO (IOMode (..), withFile)
#if defined(dbPostgres)
import Directory.Store.Postgres.Migrations
#else
import Directory.Store.SQLite.Migrations
#endif
runDirectoryMigrations :: DirectoryOpts -> ChatConfig -> DBStore -> IO ()
runDirectoryMigrations opts ChatConfig {confirmMigrations} chatStore =
migrateDBSchema
chatStore
(toDBOpts dbOptions chatSuffix False)
(Just "sx_directory_migrations")
directorySchemaMigrations
MigrationConfig {confirm, backupPath = Nothing}
>>= either (exit . ("directory migrations " <>) . show) pure
where
DirectoryOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations}} = opts
confirm = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
checkDirectoryLog :: DirectoryOpts -> ChatConfig -> IO ()
checkDirectoryLog opts cfg =
withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do
runDirectoryMigrations opts cfg st
gs <- readDirectoryLogData logFile
withActiveUser st $ \user -> withTransaction st $ \db -> do
mapM_ (verifyGroupRegistration db user) gs
putStrLn $ show (length gs) <> " group registrations OK"
importDirectoryLogToDB :: DirectoryOpts -> ChatConfig -> IO ()
importDirectoryLogToDB opts cfg = do
withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do
runDirectoryMigrations opts cfg st
gs <- readDirectoryLogData logFile
withActiveUser st $ \user -> withTransaction st $ \db -> do
forM_ gs $ \gr -> do
verifyGroupRegistration db user gr
insertGroupReg db gr
renamePath logFile (logFile ++ ".bak")
putStrLn $ show (length gs) <> " group registrations imported"
exit :: String -> IO a
exit err = putStrLn ("Error: " <> err) >> exitFailure
exportDBToDirectoryLog :: DirectoryOpts -> ChatConfig -> IO ()
exportDBToDirectoryLog opts cfg =
withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do
whenM (doesFileExist logFile) $ exit $ "directory log file " ++ logFile ++ " already exists"
runDirectoryMigrations opts cfg st
withActiveUser st $ \user -> do
gs <- withFile logFile WriteMode $ \h -> withTransaction st $ \db -> do
gs <- getAllGroupRegs_ db user
forM_ gs $ \(_, gr) -> do
verifyGroupRegistration db user gr
B.hPutStrLn h $ strEncode $ GRCreate gr
pure gs
putStrLn $ show (length gs) <> " group registrations exported"
verifyGroupRegistration :: DB.Connection -> User -> GroupReg -> IO ()
verifyGroupRegistration db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId = mId} =
runExceptT (getHostMember db supportedChatVRange user gId) >>= \case
Left e -> exit $ "error loading group " <> show gId <> " host member: " <> show e
Right GroupMember {groupMemberId = mId', memberContactId = ctId'} -> do
unless (mId == Just mId') $ exit $ "bad group " <> show gId <> " host member ID: " <> show mId'
unless (Just ctId == ctId') $ exit $ "bad group " <> show gId <> " contact ID: " <> show ctId'
withDirectoryLog :: DirectoryOpts -> (FilePath -> IO ()) -> IO ()
withDirectoryLog DirectoryOpts {directoryLog} action =
maybe (exit "directory log file not specified") action directoryLog
withChatStore :: DirectoryOpts -> (DBStore -> IO ()) -> IO ()
withChatStore DirectoryOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations, migrationBackupPath}} action =
createChatDatabase dbOptions migrationConfig >>= \case
Left e -> exit $ show e
Right ChatDatabase {chatStore, agentStore} -> do
action chatStore
closeDBStore chatStore
closeDBStore agentStore
where
migrationConfig = MigrationConfig (if yesToUpMigrations then MCYesUp else MCConsole) migrationBackupPath
withActiveUser :: DBStore -> (User -> IO ()) -> IO ()
withActiveUser st action = withTransaction st getUsers >>= maybe (exit "no active user") action . find activeUser
@@ -0,0 +1,52 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}
module Directory.Store.Postgres.Migrations where
import Data.List (sortOn)
import Data.Text (Text)
import qualified Data.Text as T
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
import Text.RawString.QQ (r)
directorySchemaMigrations :: [Migration]
directorySchemaMigrations = sortOn name $ map migration schemaMigrations
where
migration (name, up, down) = Migration {name, up, down}
schemaMigrations :: [(String, Text, Maybe Text)]
schemaMigrations =
[ ("20250924_directory_schema", m20250924_directory_schema, Just down_m20250924_directory_schema)
]
m20250924_directory_schema :: Text
m20250924_directory_schema =
T.pack
[r|
CREATE TABLE sx_directory_group_regs(
group_reg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
group_id BIGINT NOT NULL REFERENCES groups ON UPDATE RESTRICT ON DELETE CASCADE,
user_group_reg_id BIGINT NOT NULL,
contact_id BIGINT NOT NULL REFERENCES contacts(contact_id) ON UPDATE RESTRICT ON DELETE CASCADE,
owner_member_id BIGINT REFERENCES group_members(group_member_id) ON UPDATE RESTRICT ON DELETE CASCADE,
group_reg_status TEXT NOT NULL,
group_promoted SMALLINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_group_id ON sx_directory_group_regs(group_id);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_member_id ON sx_directory_group_regs(owner_member_id);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id ON sx_directory_group_regs(contact_id, user_group_reg_id);
|]
down_m20250924_directory_schema :: Text
down_m20250924_directory_schema =
T.pack
[r|
DROP INDEX idx_sx_directory_group_regs_group_id;
DROP INDEX idx_sx_directory_group_regs_owner_member_id;
DROP INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id;
DROP TABLE sx_directory_group_regs;
|]
@@ -0,0 +1,49 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}
module Directory.Store.SQLite.Migrations (directorySchemaMigrations) where
import Data.List (sortOn)
import Database.SQLite.Simple (Query (..))
import Database.SQLite.Simple.QQ (sql)
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
directorySchemaMigrations :: [Migration]
directorySchemaMigrations = sortOn name $ map migration schemaMigrations
where
migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down}
schemaMigrations :: [(String, Query, Maybe Query)]
schemaMigrations =
[ ("20250924_directory_schema", m20250924_directory_schema, Just down_m20250924_directory_schema)
]
m20250924_directory_schema :: Query
m20250924_directory_schema =
[sql|
CREATE TABLE sx_directory_group_regs(
group_reg_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON UPDATE RESTRICT ON DELETE CASCADE,
user_group_reg_id INTEGER NOT NULL,
contact_id INTEGER NOT NULL REFERENCES contacts(contact_id) ON UPDATE RESTRICT ON DELETE CASCADE,
owner_member_id INTEGER REFERENCES group_members(group_member_id) ON UPDATE RESTRICT ON DELETE CASCADE,
group_reg_status TEXT NOT NULL,
group_promoted INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_group_id ON sx_directory_group_regs(group_id);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_member_id ON sx_directory_group_regs(owner_member_id);
CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id ON sx_directory_group_regs(contact_id, user_group_reg_id);
|]
down_m20250924_directory_schema :: Query
down_m20250924_directory_schema =
[sql|
DROP INDEX idx_sx_directory_group_regs_group_id;
DROP INDEX idx_sx_directory_group_regs_owner_member_id;
DROP INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id;
DROP TABLE sx_directory_group_regs;
|]
@@ -5,27 +5,27 @@
module Directory.Util where
import qualified Control.Exception as E
import Control.Logger.Simple
import Control.Monad.Except
import Data.Text (Text)
import qualified Data.Text as T
import Simplex.Chat.Controller
import Simplex.Chat.Store.Shared (StoreError (..))
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Store.Common (withTransaction)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (tshow)
import Simplex.Messaging.Util (catchAll)
vr :: ChatController -> VersionRangeChat
vr ChatController {config = ChatConfig {chatVRange}} = chatVRange
{-# INLINE vr #-}
withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a)
withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Either String a)
withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a
withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a)
withDB :: Text -> ChatController -> (DB.Connection -> ExceptT String IO a) -> IO (Either String a)
withDB cxt ChatController {chatStore} action = do
r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
r_ <- withTransaction chatStore (runExceptT . action) `catchAll` (pure . Left . show)
case r_ of
Right r -> pure $ Just r
Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e)
Left e -> logError $ "Database error: " <> cxt <> " " <> T.pack e
Right _ -> pure ()
pure r_
+1 -1
View File
@@ -1465,7 +1465,7 @@ Get groups.
GroupsList: Groups.
- type: "groupsList"
- user: [User](./TYPES.md#user)
- groups: [[GroupInfoSummary](./TYPES.md#groupinfosummary)]
- groups: [[GroupInfo](./TYPES.md#groupinfo)]
ChatCmdError: Command error.
- type: "chatCmdError"
+2 -12
View File
@@ -89,7 +89,6 @@ This file is generated automatically.
- [GroupFeature](#groupfeature)
- [GroupFeatureEnabled](#groupfeatureenabled)
- [GroupInfo](#groupinfo)
- [GroupInfoSummary](#groupinfosummary)
- [GroupLink](#grouplink)
- [GroupLinkPlan](#grouplinkplan)
- [GroupMember](#groupmember)
@@ -2148,20 +2147,11 @@ MemberSupport:
- chatItemTTL: int64?
- uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)?
- customData: JSONObject?
- groupSummary: [GroupSummary](#groupsummary)
- membersRequireAttention: int
- viaGroupLinkUri: string?
---
## GroupInfoSummary
**Record type**:
- groupInfo: [GroupInfo](#groupinfo)
- groupSummary: [GroupSummary](#groupsummary)
- groupLink: [GroupLink](#grouplink)?
---
## GroupLink
@@ -2352,7 +2342,7 @@ Known:
## GroupSummary
**Record type**:
- currentMembers: int
- currentMembers: int64
---
-2
View File
@@ -266,7 +266,6 @@ chatTypesDocsData =
(sti @GroupFeature, STEnum, "GF", [], "", ""),
(sti @GroupFeatureEnabled, STEnum, "FE", [], "", ""),
(sti @GroupInfo, STRecord, "", [], "", ""),
(sti @GroupInfoSummary, STRecord, "", [], "", ""),
(sti @GroupLink, STRecord, "", [], "", ""),
(sti @GroupLinkPlan, STUnion, "GLP", [], "", ""),
(sti @GroupMember, STRecord, "", [], "", ""),
@@ -453,7 +452,6 @@ deriving instance Generic GroupChatScopeInfo
deriving instance Generic GroupFeature
deriving instance Generic GroupFeatureEnabled
deriving instance Generic GroupInfo
deriving instance Generic GroupInfoSummary
deriving instance Generic GroupLink
deriving instance Generic GroupLinkPlan
deriving instance Generic GroupMember
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 54a2a6c9051f610b8c7533369d9e9cce81af06ad
tag: 99f40ae109fba4e39684056fc9acf96bb6cc8c26
source-repository-package
type: git
@@ -244,7 +244,7 @@ export namespace CR {
export interface GroupsList extends Interface {
type: "groupsList"
user: T.User
groups: T.GroupInfoSummary[]
groups: T.GroupInfo[]
}
export interface Invitation extends Interface {
@@ -2444,16 +2444,11 @@ export interface GroupInfo {
chatItemTTL?: number // int64
uiThemes?: UIThemeEntityOverrides
customData?: object
groupSummary: GroupSummary
membersRequireAttention: number // int
viaGroupLinkUri?: string
}
export interface GroupInfoSummary {
groupInfo: GroupInfo
groupSummary: GroupSummary
groupLink?: GroupLink
}
export interface GroupLink {
userContactLinkId: number // int64
connLinkContact: CreatedConnLink
@@ -2605,7 +2600,7 @@ export interface GroupShortLinkData {
}
export interface GroupSummary {
currentMembers: number // int
currentMembers: number // int64
}
export interface GroupSupportChat {
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."54a2a6c9051f610b8c7533369d9e9cce81af06ad" = "02c6hh5vffm23gz2gkn6rq2ifcyj5c9s6kz0kyk055i78gsfhp43";
"https://github.com/simplex-chat/simplexmq.git"."99f40ae109fba4e39684056fc9acf96bb6cc8c26" = "04k7g4q0jlbg14wx47w076vk0q6dh709vw24n1q5im77y5z0v0ak";
"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";
+19
View File
@@ -118,6 +118,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri
Simplex.Chat.Store.Postgres.Migrations.M20250802_chat_peer_type
Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks
Simplex.Chat.Store.Postgres.Migrations.M20250919_group_summary
else
exposed-modules:
Simplex.Chat.Archive
@@ -259,6 +260,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
Simplex.Chat.Store.SQLite.Migrations.M20250813_delivery_tasks
Simplex.Chat.Store.SQLite.Migrations.M20250919_group_summary
other-modules:
Paths_simplex_chat
hs-source-dirs:
@@ -462,6 +464,7 @@ executable simplex-directory-service
Directory.Search
Directory.Service
Directory.Store
Directory.Store.Migrate
Directory.Util
Paths_simplex_chat
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
@@ -489,7 +492,17 @@ executable simplex-directory-service
, unicode-transforms ==0.4.*
default-language: Haskell2010
if flag(client_postgres)
other-modules:
Directory.Store.Postgres.Migrations
build-depends:
postgresql-simple ==0.7.*
, raw-strings-qq ==1.1.*
cpp-options: -DdbPostgres
else
other-modules:
Directory.Store.SQLite.Migrations
build-depends:
sqlcipher-simple ==0.4.*
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
@@ -550,6 +563,7 @@ test-suite simplex-chat-test
Directory.Search
Directory.Service
Directory.Store
Directory.Store.Migrate
Directory.Util
Paths_simplex_chat
if flag(client_postgres)
@@ -603,10 +617,15 @@ test-suite simplex-chat-test
, uri-bytestring >=0.3.3.1 && <0.4
default-language: Haskell2010
if flag(client_postgres)
other-modules:
Directory.Store.Postgres.Migrations
build-depends:
postgresql-simple ==0.7.*
, raw-strings-qq ==1.1.*
cpp-options: -DdbPostgres
else
other-modules:
Directory.Store.SQLite.Migrations
build-depends:
sqlcipher-simple ==0.4.*
if impl(ghc >= 9.6.2)
+7 -7
View File
@@ -313,8 +313,8 @@ data ChatCommand
| APIGetAppSettings (Maybe AppSettings)
| APIGetChatTags UserId
| APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
| APIGetChat {chatRef :: ChatRef, contentTag :: Maybe MsgContentTag, chatPagination :: ChatPagination, search :: Maybe String}
| APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe String}
| APIGetChat {chatRef :: ChatRef, contentTag :: Maybe MsgContentTag, chatPagination :: ChatPagination, search :: Maybe Text}
| APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe Text}
| APIGetChatItemInfo {chatRef :: ChatRef, chatItemId :: ChatItemId}
| APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage}
| APICreateChatTag ChatTagData
@@ -511,8 +511,8 @@ data ChatCommand
| ClearGroup GroupName
| ListMembers GroupName
| ListMemberSupportChats GroupName
| APIListGroups {userId :: UserId, contactId_ :: Maybe ContactId, search :: Maybe String}
| ListGroups (Maybe ContactName) (Maybe String)
| APIListGroups {userId :: UserId, contactId_ :: Maybe ContactId, search :: Maybe Text}
| ListGroups (Maybe ContactName) (Maybe Text)
| UpdateGroupNames GroupName GroupProfile
| ShowGroupProfile GroupName
| UpdateGroupDescription GroupName (Maybe Text)
@@ -524,7 +524,7 @@ data ChatCommand
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text}
| ClearNoteFolder
| LastChats (Maybe Int) -- UserId (not used in UI)
| LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI)
| LastMessages (Maybe ChatName) Int (Maybe Text) -- UserId (not used in UI)
| LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI)
| ShowChatItem (Maybe ChatItemId) -- UserId (not used in UI)
| ShowChatItemInfo ChatName Text
@@ -647,7 +647,7 @@ data ChatResponse
| CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64}
| CRNetworkConfig {networkConfig :: NetworkConfig}
| CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile}
| CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary}
| CRGroupInfo {user :: User, groupInfo :: GroupInfo}
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
| CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo}
| CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
@@ -682,7 +682,7 @@ data ChatResponse
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact}
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool}
| CRGroupsList {user :: User, groups :: [GroupInfoSummary]}
| CRGroupsList {user :: User, groups :: [GroupInfo]}
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
| CRFileTransferStatusXFTP User AChatItem
+9 -10
View File
@@ -1601,9 +1601,8 @@ processChatCommand vr nm = \case
case activeConn of
Just conn -> getConnQueueInfo user conn
Nothing -> throwChatError $ CEContactNotActive ct
APIGroupInfo gId -> withUser $ \user -> do
(g, s) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId)
pure $ CRGroupInfo user g s
APIGroupInfo gId -> withUser $ \user ->
CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db vr user gId)
APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m)
@@ -2570,7 +2569,7 @@ processChatCommand vr nm = \case
let memberSupportChats = filter (isJust . supportChat) members
pure $ CRMemberSupportChats user gInfo memberSupportChats
APIListGroups userId contactId_ search_ -> withUserId userId $ \user ->
CRGroupsList user <$> withFastStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_)
CRGroupsList user <$> withFastStore' (\db -> getBaseGroupDetails db vr user contactId_ search_)
ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do
ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName
processChatCommand vr nm $ APIListGroups userId (contactId' <$> ct_) search_
@@ -2923,7 +2922,7 @@ processChatCommand vr nm = \case
ShowVersion -> do
-- simplexmqCommitQ makes iOS builds crash m(
let versionInfo = coreVersionInfo ""
chatMigrations <- map upMigration <$> withFastStore' getCurrentMigrations
chatMigrations <- map upMigration <$> withFastStore' (getCurrentMigrations Nothing)
agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
DebugLocks -> lift $ do
@@ -4502,8 +4501,8 @@ chatCommandP =
<*> (A.space *> paginationByTimeP <|> pure (PTLast 5000))
<*> (A.space *> jsonP <|> pure clqNoFilters)
),
"/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)),
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)),
"/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> textP)),
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)),
"/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal),
"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
"/_create tag " *> (APICreateChatTag <$> jsonP),
@@ -4660,8 +4659,8 @@ chatCommandP =
"/clear " *> char_ '@' *> (ClearContact <$> displayNameP),
("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayNameP),
"/member support chats #" *> (ListMemberSupportChats <$> displayNameP),
"/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> stringP)),
("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> stringP)),
"/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> textP)),
("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> textP)),
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayNameP <* A.space <*> groupProfile),
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayNameP),
@@ -4715,7 +4714,7 @@ chatCommandP =
"/feed " *> (SendMessageBroadcast . MCText <$> msgTextP),
("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))),
("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing),
("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))),
("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> textP))),
"/last_item_id" *> (LastChatItemId <$> optional (A.space *> chatNameP) <*> (A.space *> A.decimal <|> pure 0)),
"/show" *> (ShowLiveItems <$> (A.space *> onOffP <|> pure True)),
"/show " *> (ShowChatItem . Just <$> A.decimal),
+2 -2
View File
@@ -16,7 +16,7 @@ module Simplex.Chat.Store.Connections
getConnectionEntityViaShortLink,
getContactConnEntityByConnReqHash,
getConnectionsToSubscribe,
unsetConnectionToSubscribe
unsetConnectionToSubscribe,
)
where
@@ -143,7 +143,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+165 -165
View File
@@ -14,9 +14,9 @@ module Simplex.Chat.Store.ContactRequest
setContactAcceptedXContactId,
setBusinessChatAcceptedXContactId,
setRequestSharedMsgIdForContact,
setRequestSharedMsgIdForGroup
setRequestSharedMsgIdForGroup,
)
where
where
import Control.Monad
import Control.Monad.Except
@@ -24,11 +24,11 @@ import Control.Monad.IO.Class
import Crypto.Random (ChaChaDRG)
import Data.Int (Int64)
import Data.Time.Clock (getCurrentTime)
import Simplex.Chat.Protocol (businessChatsVersion, MsgContent)
import Simplex.Chat.Protocol (MsgContent, businessChatsVersion)
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Shared
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (InvitationId)
@@ -47,21 +47,21 @@ import Database.SQLite.Simple.QQ (sql)
#endif
createOrUpdateContactRequest ::
DB.Connection
-> TVar ChaChaDRG
-> VersionRangeChat
-> User
-> Int64
-> UserContactLink
-> Bool
-> InvitationId
-> VersionRangeChat
-> Profile
-> Maybe XContactId
-> Maybe SharedMsgId
-> Maybe (SharedMsgId, MsgContent)
-> PQSupport
-> ExceptT StoreError IO RequestStage
DB.Connection ->
TVar ChaChaDRG ->
VersionRangeChat ->
User ->
Int64 ->
UserContactLink ->
Bool ->
InvitationId ->
VersionRangeChat ->
Profile ->
Maybe XContactId ->
Maybe SharedMsgId ->
Maybe (SharedMsgId, MsgContent) ->
PQSupport ->
ExceptT StoreError IO RequestStage
createOrUpdateContactRequest
db
gVar
@@ -86,61 +86,62 @@ createOrUpdateContactRequest
Just ct -> do
cr <- liftIO $ getContactRequestByXContactId xContactId
pure $ RSAcceptedRequest cr (REContact ct)
Nothing -> liftIO (getAcceptedBusinessChat xContactId) >>= \case
Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
clientMember <- getGroupMemberByMemberId db vr user gInfo customerId
cr <- liftIO $ getContactRequestByXContactId xContactId
pure $ RSAcceptedRequest cr (REBusinessChat gInfo clientMember)
Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest
-- 2) if no legacy accepted contact or business chat was found, next we try to find an existing request
Nothing ->
liftIO (getContactRequestByXContactId xContactId) >>= \case
-- 3a) if request was found, we update it
Just cr -> updateContactRequest cr
-- 3b) if no request was found, we create a new contact request
Nothing -> createContactRequest
where
getAcceptedContact :: XContactId -> IO (Maybe Contact)
getAcceptedContact xContactId = do
ct_ <-
maybeFirstRow (toContact vr user []) $
Nothing ->
liftIO (getAcceptedBusinessChat xContactId) >>= \case
Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
clientMember <- getGroupMemberByMemberId db vr user gInfo customerId
cr <- liftIO $ getContactRequestByXContactId xContactId
pure $ RSAcceptedRequest cr (REBusinessChat gInfo clientMember)
Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest
-- 2) if no legacy accepted contact or business chat was found, next we try to find an existing request
Nothing ->
liftIO (getContactRequestByXContactId xContactId) >>= \case
-- 3a) if request was found, we update it
Just cr -> updateContactRequest cr
-- 3b) if no request was found, we create a new contact request
Nothing -> createContactRequest
where
getAcceptedContact :: XContactId -> IO (Maybe Contact)
getAcceptedContact xContactId = do
ct_ <-
maybeFirstRow (toContact vr user []) $
DB.query
db
[sql|
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
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.snd_file_id, c.rcv_file_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 contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, xContactId)
mapM (addDirectChatTags db) ct_
getAcceptedBusinessChat :: XContactId -> IO (Maybe GroupInfo)
getAcceptedBusinessChat xContactId = do
g_ <-
maybeFirstRow (toGroupInfo vr userContactId []) $
DB.query
db
(groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?")
(xContactId, userId, userContactId)
mapM (addGroupChatTags db) g_
getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest)
getContactRequestByXContactId xContactId =
maybeFirstRow toContactRequest $
DB.query
db
[sql|
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
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.snd_file_id, c.rcv_file_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 contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, xContactId)
mapM (addDirectChatTags db) ct_
getAcceptedBusinessChat :: XContactId -> IO (Maybe GroupInfo)
getAcceptedBusinessChat xContactId = do
g_ <-
maybeFirstRow (toGroupInfo vr userContactId []) $
DB.query
db
(groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?")
(xContactId, userId, userContactId)
mapM (addGroupChatTags db) g_
getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest)
getContactRequestByXContactId xContactId =
maybeFirstRow toContactRequest $
DB.query
db
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id,
cr.contact_id, cr.business_group_id, cr.user_contact_link_id,
@@ -154,79 +155,79 @@ createOrUpdateContactRequest
AND cr.xcontact_id = ?
LIMIT 1
|]
(userId, xContactId)
createContactRequest :: ExceptT StoreError IO RequestStage
createContactRequest = do
currentTs <- liftIO $ getCurrentTime
ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
liftIO $
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs)
profileId <- liftIO $ insertedRowId db
liftIO $
DB.execute
db
[sql|
(userId, xContactId)
createContactRequest :: ExceptT StoreError IO RequestStage
createContactRequest = do
currentTs <- liftIO $ getCurrentTime
ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
liftIO $
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs)
profileId <- liftIO $ insertedRowId db
liftIO $
DB.execute
db
[sql|
INSERT INTO contact_requests
(user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id,
created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (uclId, Binary invId, minV, maxV, profileId, ldn, userId)
:. (currentTs, currentTs, xContactId_, welcomeMsgId_, fst <$> requestMsg_, pqSup)
)
contactRequestId <- liftIO $ insertedRowId db
createRequestEntity ldn profileId contactRequestId currentTs
where
createRequestEntity ldn profileId contactRequestId currentTs
| businessAddress =
if isSimplexTeam && maxV < businessChatsVersion
then createContact'
else createBusinessChat
| otherwise = createContact'
where
createContact' = do
let ctUserPreferences = newContactUserPrefs user profile
liftIO $
DB.execute
db
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?,?)"
(profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True, contactRequestId)
contactId <- liftIO $ insertedRowId db
liftIO $
DB.execute
db
"UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?"
(contactId, contactRequestId)
ucr <- getContactRequest db user contactRequestId
ct <- getContact db vr user contactId
pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct)
createBusinessChat = do
let groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs $ preferences' user
(gInfo@GroupInfo {groupId}, clientMember) <-
createBusinessRequestGroup db vr gVar user cReqChatVRange profile profileId ldn groupPreferences
liftIO $
DB.execute
db
"UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ?"
(groupId, contactRequestId)
ucr <- getContactRequest db user contactRequestId
pure $ RSCurrentRequest Nothing ucr (Just $ REBusinessChat gInfo clientMember)
updateContactRequest :: UserContactRequest -> ExceptT StoreError IO RequestStage
updateContactRequest ucr@UserContactRequest {contactRequestId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do
currentTs <- liftIO getCurrentTime
liftIO $ updateProfile currentTs
updateRequest currentTs
ucr' <- getContactRequest db user contactRequestId
re_ <- getRequestEntity ucr'
pure $ RSCurrentRequest (Just ucr) ucr' re_
where
updateProfile currentTs =
DB.execute
db
[sql|
( (uclId, Binary invId, minV, maxV, profileId, ldn, userId)
:. (currentTs, currentTs, xContactId_, welcomeMsgId_, fst <$> requestMsg_, pqSup)
)
contactRequestId <- liftIO $ insertedRowId db
createRequestEntity ldn profileId contactRequestId currentTs
where
createRequestEntity ldn profileId contactRequestId currentTs
| businessAddress =
if isSimplexTeam && maxV < businessChatsVersion
then createContact'
else createBusinessChat
| otherwise = createContact'
where
createContact' = do
let ctUserPreferences = newContactUserPrefs user profile
liftIO $
DB.execute
db
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?,?)"
(profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True, contactRequestId)
contactId <- liftIO $ insertedRowId db
liftIO $
DB.execute
db
"UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?"
(contactId, contactRequestId)
ucr <- getContactRequest db user contactRequestId
ct <- getContact db vr user contactId
pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct)
createBusinessChat = do
let groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs $ preferences' user
(gInfo@GroupInfo {groupId}, clientMember) <-
createBusinessRequestGroup db vr gVar user cReqChatVRange profile profileId ldn groupPreferences
liftIO $
DB.execute
db
"UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ?"
(groupId, contactRequestId)
ucr <- getContactRequest db user contactRequestId
pure $ RSCurrentRequest Nothing ucr (Just $ REBusinessChat gInfo clientMember)
updateContactRequest :: UserContactRequest -> ExceptT StoreError IO RequestStage
updateContactRequest ucr@UserContactRequest {contactRequestId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do
currentTs <- liftIO getCurrentTime
liftIO $ updateProfile currentTs
updateRequest currentTs
ucr' <- getContactRequest db user contactRequestId
re_ <- getRequestEntity ucr'
pure $ RSCurrentRequest (Just ucr) ucr' re_
where
updateProfile currentTs =
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?,
full_name = ?,
@@ -241,21 +242,20 @@ createOrUpdateContactRequest
AND contact_request_id = ?
)
|]
(displayName, fullName, shortDescr, image, contactLink, currentTs, userId, contactRequestId)
updateRequest currentTs =
if displayName == oldDisplayName
then
liftIO $
DB.execute
db
[sql|
(displayName, fullName, shortDescr, image, contactLink, currentTs, userId, contactRequestId)
updateRequest currentTs =
if displayName == oldDisplayName
then
liftIO $
DB.execute
db
[sql|
UPDATE contact_requests
SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ?
WHERE user_id = ? AND contact_request_id = ?
|]
(Binary invId, pqSup, minV, maxV, currentTs, userId, contactRequestId)
else
ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
(Binary invId, pqSup, minV, maxV, currentTs, userId, contactRequestId)
else ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
liftIO $ do
DB.execute
db
@@ -276,21 +276,21 @@ createOrUpdateContactRequest
|]
(ldn, currentTs, contactId)
safeDeleteLDN db user oldLdn
getRequestEntity :: UserContactRequest -> ExceptT StoreError IO (Maybe RequestEntity)
getRequestEntity UserContactRequest {contactRequestId, contactId_, businessGroupId_} =
case (contactId_, businessGroupId_) of
(Just contactId, Nothing) -> do
ct <- getContact db vr user contactId
pure $ Just (REContact ct)
(Nothing, Just businessGroupId) -> do
gInfo <- getGroupInfo db vr user businessGroupId
case gInfo of
GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
clientMember <- getGroupMemberByMemberId db vr user gInfo customerId
pure $ Just (REBusinessChat gInfo clientMember)
_ -> throwError SEInvalidBusinessChatContactRequest
(Nothing, Nothing) -> pure Nothing
_ -> throwError $ SEInvalidContactRequestEntity contactRequestId
getRequestEntity :: UserContactRequest -> ExceptT StoreError IO (Maybe RequestEntity)
getRequestEntity UserContactRequest {contactRequestId, contactId_, businessGroupId_} =
case (contactId_, businessGroupId_) of
(Just contactId, Nothing) -> do
ct <- getContact db vr user contactId
pure $ Just (REContact ct)
(Nothing, Just businessGroupId) -> do
gInfo <- getGroupInfo db vr user businessGroupId
case gInfo of
GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
clientMember <- getGroupMemberByMemberId db vr user gInfo customerId
pure $ Just (REBusinessChat gInfo clientMember)
_ -> throwError SEInvalidBusinessChatContactRequest
(Nothing, Nothing) -> pure Nothing
_ -> throwError $ SEInvalidContactRequestEntity contactRequestId
setContactAcceptedXContactId :: DB.Connection -> Contact -> XContactId -> IO ()
setContactAcceptedXContactId db Contact {contactId} xContactId =
+45 -63
View File
@@ -70,9 +70,7 @@ module Simplex.Chat.Store.Groups
cleanupHostGroupLinkConn,
deleteGroup,
getUserGroupsToSubscribe,
getUserGroupDetails,
getUserGroupsWithSummary,
getGroupSummary,
getBaseGroupDetails,
getContactGroupPreferences,
getGroupInvitation,
createNewContactMember,
@@ -170,6 +168,7 @@ import Data.List (partition, sortOn)
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
import Data.Ord (Down (..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime (..), getCurrentTime)
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Messages
@@ -362,6 +361,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
groupSummary = GroupSummary 1,
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing
@@ -436,6 +436,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
groupSummary = GroupSummary 2,
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing
@@ -934,65 +935,21 @@ getUserGroupsToSubscribe db user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroupToSubscribe db user) groupIds
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
g_ <-
map (toGroupInfo vr userContactId [])
<$> DB.query
db
[sql|
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
FROM groups g
JOIN group_profiles gp USING (group_profile_id)
JOIN group_members mu USING (group_id)
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
WHERE g.user_id = ? AND mu.contact_id = ?
AND (LOWER(gp.display_name) LIKE '%' || ? || '%'
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
OR LOWER(gp.description) LIKE '%' || ? || '%'
)
|]
(userId, userContactId, search, search, search, search)
mapM (addGroupChatTags db) g_
getBaseGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe Text -> IO [GroupInfo]
getBaseGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
map (toGroupInfo vr userContactId [])
<$> DB.query db (groupInfoQuery <> " " <> condition) (userId, userContactId, search, search, search, search)
where
search = maybe "" (map toLower) search_
getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfoSummary]
getUserGroupsWithSummary db vr user _contactId_ search_ = do
gs <- getUserGroupDetails db vr user _contactId_ search_
forM gs $ \g@GroupInfo {groupId} -> do
s <- getGroupSummary db user groupId
link_ <- eitherToMaybe <$> runExceptT (getGroupLink db user g)
pure $ GIS g s link_
-- the statuses on non-current members should match memberCurrent' function
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
getGroupSummary db User {userId} groupId = do
currentMembers_ <-
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT count (m.group_member_id)
FROM groups g
JOIN group_members m USING (group_id)
WHERE g.user_id = ?
AND g.group_id = ?
AND m.member_status NOT IN (?,?,?,?,?)
|]
(userId, groupId, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited)
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
condition =
[sql|
WHERE g.user_id = ? AND mu.contact_id = ?
AND (LOWER(gp.display_name) LIKE '%' || ? || '%'
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
OR LOWER(gp.description) LIKE '%' || ? || '%'
)
|]
search = maybe "" (T.map toLower) search_
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)]
getContactGroupPreferences db User {userId} Contact {contactId} = do
@@ -1915,7 +1872,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -2590,7 +2547,32 @@ createMemberContact
quotaErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
pure
Contact
{ contactId,
localDisplayName,
profile = memberProfile,
activeConn = Just ctConn,
viaGroup = Nothing,
contactUsed = True,
contactStatus = CSActive,
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
preparedContact = Nothing,
contactRequestId = Nothing,
contactGroupMemberId = Just groupMemberId,
contactGrpInvSent = False,
groupDirectInv = Nothing,
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
chatDeleted = False,
customData = Nothing
}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
+31 -31
View File
@@ -1167,7 +1167,7 @@ checkContactHasItems db User {userId} Contact {contactId} =
"SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id = ?)"
(userId, contactId)
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChat db vr user contactId pagination search_ = do
let search = fromMaybe "" search_
ct <- getContact db vr user contactId
@@ -1177,18 +1177,18 @@ getDirectChat db vr user contactId pagination search_ = do
CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct beforeId count search
CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search
CPInitial count -> do
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
getDirectChatInitial_ db user ct count
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect)
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO (Chat 'CTDirect)
getDirectChatLast_ db user ct count search = do
ciIds <- getDirectChatItemIdsLast_ db user ct count search
ts <- getCurrentTime
cis <- mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats
getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId]
getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO [ChatItemId]
getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search =
map fromOnly
<$> DB.query
@@ -1245,7 +1245,7 @@ getDirectChatItemLast db user@User {userId} contactId = do
(userId, contactId)
getDirectChatItem db user contactId chatItemId
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do
afterCI <- getDirectChatItem db user contactId afterId
ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search
@@ -1253,7 +1253,7 @@ getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do
cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) cis emptyChatStats
getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId]
getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId]
getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search =
map fromOnly
<$> DB.query
@@ -1268,7 +1268,7 @@ getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search =
|]
(userId, contactId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count)
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do
beforeCI <- getDirectChatItem db user contactId beforeId
ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search
@@ -1276,7 +1276,7 @@ getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do
cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats
getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId]
getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId]
getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search =
map fromOnly
<$> DB.query
@@ -1291,12 +1291,12 @@ getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search =
|]
(userId, contactId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count)
getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround_ db user ct aroundId count search = do
stats <- liftIO $ getContactStats_ db user ct
getDirectChatAround' db user ct aroundId count search stats
getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround' db user ct@Contact {contactId} aroundId count search stats = do
aroundCI <- getDirectChatItem db user contactId aroundId
beforeIds <- liftIO $ getDirectCIsBefore_ db user ct aroundCI count search
@@ -1404,7 +1404,7 @@ getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do
:. (userId, contactId, ciCreatedAt afterCI, cChatItemId afterCI)
)
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe GroupChatScope -> Maybe MsgContentTag -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe GroupChatScope -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChat db vr user groupId scope_ contentFilter pagination search_ = do
let search = fromMaybe "" search_
g <- getGroupInfo db vr user groupId
@@ -1415,7 +1415,7 @@ getGroupChat db vr user groupId scope_ contentFilter pagination search_ = do
CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g scopeInfo contentFilter beforeId count search
CPAround aroundId count -> getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search
CPInitial count -> do
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
getGroupChatInitial_ db user g scopeInfo contentFilter count
getCreateGroupChatScopeInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo
@@ -1466,7 +1466,7 @@ getGroupChatScopeForItem_ db itemId =
(Nothing, Nothing) -> Nothing
(Nothing, Just _) -> Nothing -- shouldn't happen
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatLast_ db user g scopeInfo_ contentFilter count search stats = do
ciIds <- getGroupChatItemIDs db user g scopeInfo_ contentFilter GRLast count search
ts <- liftIO getCurrentTime
@@ -1475,7 +1475,7 @@ getGroupChatLast_ db user g scopeInfo_ contentFilter count search stats = do
data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId
getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> String -> ExceptT StoreError IO [ChatItemId]
getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> Text -> ExceptT StoreError IO [ChatItemId]
getGroupChatItemIDs db User {userId} GroupInfo {groupId} scopeInfo_ contentFilter range count search = case (scopeInfo_, contentFilter) of
(Nothing, Nothing) ->
liftIO $
@@ -1520,7 +1520,7 @@ getGroupChatItemIDs db User {userId} GroupInfo {groupId} scopeInfo_ contentFilte
orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId))
rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId]
rangeQuery c p ob
| null search = searchQuery "" ()
| T.null search = searchQuery "" ()
| otherwise = searchQuery " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' " (Only search)
where
searchQuery :: ToRow p' => Query -> p' -> IO [ChatItemId]
@@ -1570,7 +1570,7 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do
(userId, groupId, groupMemberId)
getGroupChatItem db user groupId chatItemId
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatAfter_ db user g@GroupInfo {groupId} scopeInfo contentFilter afterId count search = do
afterCI <- getGroupChatItem db user groupId afterId
let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI)
@@ -1579,7 +1579,7 @@ getGroupChatAfter_ db user g@GroupInfo {groupId} scopeInfo contentFilter afterId
cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds
pure $ Chat (GroupChat g scopeInfo) cis emptyChatStats
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatBefore_ db user g@GroupInfo {groupId} scopeInfo contentFilter beforeId count search = do
beforeCI <- getGroupChatItem db user groupId beforeId
let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI)
@@ -1588,12 +1588,12 @@ getGroupChatBefore_ db user g@GroupInfo {groupId} scopeInfo contentFilter before
cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds
pure $ Chat (GroupChat g scopeInfo) (reverse cis) emptyChatStats
getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do
stats <- getGroupStats_ db user g scopeInfo
getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats
getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats = do
aroundCI <- getGroupCIWithReactions db user g aroundId
let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI)
@@ -1736,7 +1736,7 @@ getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do
:. (userId, groupId, chatItemTs afterCI, cChatItemId afterCI)
)
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChat db user folderId pagination search_ = do
let search = fromMaybe "" search_
nf <- getNoteFolder db user folderId
@@ -1746,17 +1746,17 @@ getLocalChat db user folderId pagination search_ = do
CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf beforeId count search
CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search
CPInitial count -> do
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
getLocalChatInitial_ db user nf count
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal)
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO (Chat 'CTLocal)
getLocalChatLast_ db user nf count search = do
ciIds <- getLocalChatItemIdsLast_ db user nf count search
ts <- getCurrentTime
cis <- mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats
getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId]
getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO [ChatItemId]
getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search =
map fromOnly
<$> DB.query
@@ -1797,7 +1797,7 @@ safeToLocalItem currentTs itemId = \case
file = Nothing
}
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do
afterCI <- getLocalChatItem db user noteFolderId afterId
ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search
@@ -1805,7 +1805,7 @@ getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = d
cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) cis emptyChatStats
getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId]
getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId]
getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search =
map fromOnly
<$> DB.query
@@ -1820,7 +1820,7 @@ getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count searc
|]
(userId, noteFolderId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count)
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do
beforeCI <- getLocalChatItem db user noteFolderId beforeId
ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search
@@ -1828,7 +1828,7 @@ getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search =
cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats
getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId]
getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId]
getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search =
map fromOnly
<$> DB.query
@@ -1843,12 +1843,12 @@ getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count sea
|]
(userId, noteFolderId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count)
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround_ db user nf aroundId count search = do
stats <- liftIO $ getLocalStats_ db user nf
getLocalChatAround' db user nf aroundId count search stats
getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround' db user nf@NoteFolder {noteFolderId} aroundId count search stats = do
aroundCI <- getLocalChatItem db user noteFolderId aroundId
beforeIds <- liftIO $ getLocalCIsBefore_ db user nf aroundCI count search
@@ -2370,7 +2370,7 @@ toGroupChatItem
ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem]
getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe Text -> ExceptT StoreError IO [AChatItem]
getAllChatItems db vr user@User {userId} pagination search_ = do
itemRefs <-
rights . map toChatItemRef <$> case pagination of
@@ -2379,7 +2379,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId
CPAround aroundId count -> liftIO . getAllChatItemsAround_ aroundId count . aChatItemTs =<< getAChatItem_ aroundId
CPInitial count -> do
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
liftIO getFirstUnreadItemId_ >>= \case
Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId
Nothing -> liftIO $ getAllChatItemsLast_ count
@@ -17,6 +17,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests
import Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri
import Simplex.Chat.Store.Postgres.Migrations.M20250802_chat_peer_type
import Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks
import Simplex.Chat.Store.Postgres.Migrations.M20250919_group_summary
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -33,7 +34,8 @@ schemaMigrations =
("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests),
("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri),
("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type),
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks)
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks),
("20250919_group_summary", m20250919_group_summary, Just down_m20250919_group_summary)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,116 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20250919_group_summary where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
m20250919_group_summary :: Text
m20250919_group_summary =
T.pack
[r|
ALTER TABLE groups ADD COLUMN summary_current_members_count BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_groups_summary_current_members_count ON groups(summary_current_members_count);
CREATE FUNCTION is_current_member(p_status TEXT) RETURNS BOOLEAN
LANGUAGE plpgsql AS $$
BEGIN
RETURN p_status IN (
'introduced',
'intro-inv',
'accepted',
'announced',
'connected',
'complete',
'creator'
);
END;
$$;
UPDATE groups g
SET summary_current_members_count = COALESCE(c.cnt, 0)
FROM (
SELECT group_id, COUNT(group_member_id) AS cnt
FROM group_members
WHERE is_current_member(member_status) = TRUE
GROUP BY group_id
) c
WHERE g.group_id = c.group_id;
CREATE FUNCTION on_group_members_insert_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
IF is_current_member(NEW.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION on_group_members_delete_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
IF is_current_member(OLD.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END IF;
RETURN OLD;
END;
$$;
CREATE FUNCTION on_group_members_update_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
DECLARE
was_active BOOLEAN;
is_active BOOLEAN;
BEGIN
was_active := is_current_member(OLD.member_status);
is_active := is_current_member(NEW.member_status);
IF was_active != is_active THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(CASE WHEN is_active THEN 1 ELSE -1 END)
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER tr_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_insert_update_summary();
CREATE TRIGGER tr_group_members_delete_update_summary
AFTER DELETE ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_delete_update_summary();
CREATE TRIGGER tr_group_members_update_update_summary
AFTER UPDATE ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_update_update_summary();
|]
down_m20250919_group_summary :: Text
down_m20250919_group_summary =
T.pack
[r|
DROP TRIGGER tr_group_members_insert_update_summary ON group_members;
DROP TRIGGER tr_group_members_delete_update_summary ON group_members;
DROP TRIGGER tr_group_members_update_update_summary ON group_members;
DROP FUNCTION on_group_members_insert_update_summary;
DROP FUNCTION on_group_members_delete_update_summary;
DROP FUNCTION on_group_members_update_update_summary;
DROP FUNCTION is_current_member;
DROP INDEX idx_groups_summary_current_members_count;
ALTER TABLE groups DROP COLUMN summary_current_members_count;
|]
@@ -15,6 +15,76 @@ SET row_security = off;
CREATE SCHEMA test_chat_schema;
CREATE FUNCTION test_chat_schema.is_current_member(p_status text) RETURNS boolean
LANGUAGE plpgsql
AS $$
BEGIN
RETURN p_status IN (
'introduced',
'intro-inv',
'accepted',
'announced',
'connected',
'complete',
'creator'
);
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_delete_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF is_current_member(OLD.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END IF;
RETURN OLD;
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_insert_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF is_current_member(NEW.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_update_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
was_active BOOLEAN;
is_active BOOLEAN;
BEGIN
was_active := is_current_member(OLD.member_status);
is_active := is_current_member(NEW.member_status);
IF was_active != is_active THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(CASE WHEN is_active THEN 1 ELSE -1 END)
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
SET default_table_access_method = heap;
@@ -716,7 +786,8 @@ CREATE TABLE test_chat_schema.groups (
welcome_shared_msg_id bytea,
request_shared_msg_id bytea,
conn_link_prepared_connection smallint DEFAULT 0 NOT NULL,
via_group_link_uri bytea
via_group_link_uri bytea,
summary_current_members_count bigint DEFAULT 0 NOT NULL
);
@@ -2064,6 +2135,10 @@ CREATE INDEX idx_groups_inv_queue_info ON test_chat_schema.groups USING btree (i
CREATE INDEX idx_groups_summary_current_members_count ON test_chat_schema.groups USING btree (summary_current_members_count);
CREATE INDEX idx_groups_via_group_link_uri_hash ON test_chat_schema.groups USING btree (user_id, via_group_link_uri_hash);
@@ -2248,6 +2323,18 @@ CREATE INDEX note_folders_user_id ON test_chat_schema.note_folders USING btree (
CREATE TRIGGER tr_group_members_delete_update_summary AFTER DELETE ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_delete_update_summary();
CREATE TRIGGER tr_group_members_insert_update_summary AFTER INSERT ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_insert_update_summary();
CREATE TRIGGER tr_group_members_update_update_summary AFTER UPDATE ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_update_update_summary();
ALTER TABLE ONLY test_chat_schema.calls
ADD CONSTRAINT calls_chat_item_id_fkey FOREIGN KEY (chat_item_id) REFERENCES test_chat_schema.chat_items(chat_item_id) ON DELETE CASCADE;
+3 -1
View File
@@ -140,6 +140,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests
import Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
import Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
import Simplex.Chat.Store.SQLite.Migrations.M20250813_delivery_tasks
import Simplex.Chat.Store.SQLite.Migrations.M20250919_group_summary
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -279,7 +280,8 @@ schemaMigrations =
("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests),
("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri),
("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type),
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks)
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks),
("20250919_group_summary", m20250919_group_summary, Just down_m20250919_group_summary)
]
-- | The list of migrations in ascending order by date
@@ -104,8 +104,6 @@ CREATE INDEX idx_delivery_tasks_next_for_job_scope_sender ON delivery_tasks(
);
CREATE INDEX idx_delivery_tasks_created_at ON delivery_tasks(created_at);
CREATE TABLE delivery_jobs (
delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -135,8 +133,6 @@ CREATE INDEX idx_delivery_jobs_next ON delivery_jobs(
);
CREATE INDEX idx_delivery_jobs_created_at ON delivery_jobs(created_at);
ALTER TABLE messages ADD COLUMN broker_ts TEXT;
|]
@@ -145,8 +141,6 @@ down_m20250813_delivery_tasks =
[sql|
ALTER TABLE messages DROP COLUMN broker_ts;
DROP INDEX idx_delivery_jobs_group_id;
DROP INDEX idx_delivery_jobs_job_scope_support_gm_id;
DROP INDEX idx_delivery_jobs_single_sender_group_member_id;
@@ -156,8 +150,6 @@ DROP INDEX idx_delivery_jobs_created_at;
DROP TABLE delivery_jobs;
DROP INDEX idx_delivery_tasks_group_id;
DROP INDEX idx_delivery_tasks_job_scope_support_gm_id;
DROP INDEX idx_delivery_tasks_sender_group_member_id;
@@ -0,0 +1,95 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250919_group_summary where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20250919_group_summary :: Query
m20250919_group_summary =
[sql|
ALTER TABLE groups ADD COLUMN summary_current_members_count INTEGER NOT NULL DEFAULT 0;
CREATE INDEX idx_groups_summary_current_members_count ON groups(summary_current_members_count);
CREATE TABLE group_member_status_predicates(
member_status TEXT NOT NULL PRIMARY KEY,
current_member INTEGER NOT NULL DEFAULT 0
);
INSERT INTO group_member_status_predicates(member_status, current_member)
VALUES
('rejected', 0),
('removed', 0),
('left', 0),
('deleted', 0),
('unknown', 0),
('invited', 0),
('pending_approval', 0),
('pending_review', 0),
('introduced', 1),
('intro-inv', 1),
('accepted', 1),
('announced', 1),
('connected', 1),
('complete', 1),
('creator', 1);
UPDATE groups
SET summary_current_members_count = c.cnt
FROM (
SELECT m.group_id, COUNT(m.group_member_id) AS cnt
FROM group_members m
JOIN group_member_status_predicates p ON m.member_status = p.member_status
WHERE p.current_member = 1
GROUP BY m.group_id
) AS c
WHERE groups.group_id = c.group_id;
CREATE TRIGGER on_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END;
CREATE TRIGGER on_group_members_delete_update_summary
AFTER DELETE ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = OLD.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END;
CREATE TRIGGER on_group_members_update_update_summary
AFTER UPDATE ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = OLD.member_status AND current_member = 1)
!= EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(
CASE WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
THEN 1 ELSE -1 END
)
WHERE group_id = NEW.group_id;
END;
|]
down_m20250919_group_summary :: Query
down_m20250919_group_summary =
[sql|
DROP TRIGGER on_group_members_insert_update_summary;
DROP TRIGGER on_group_members_delete_update_summary;
DROP TRIGGER on_group_members_update_update_summary;
DROP TABLE group_member_status_predicates;
DROP INDEX idx_groups_summary_current_members_count;
ALTER TABLE groups DROP COLUMN summary_current_members_count;
|]
@@ -48,6 +48,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO groups
@@ -57,6 +82,30 @@ Query:
Plan:
Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
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.snd_file_id, c.rcv_file_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 contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT
-- GroupInfo
@@ -65,7 +114,7 @@ Query:
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -192,6 +241,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO group_members
@@ -201,6 +275,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO groups
@@ -210,30 +309,6 @@ Query:
Plan:
Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
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.snd_file_id, c.rcv_file_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 contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT COUNT(1)
FROM chat_items i
@@ -374,6 +449,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO group_members
@@ -383,6 +483,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO group_members
@@ -392,6 +517,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO messages (
@@ -851,6 +1001,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO groups
@@ -1024,7 +1199,7 @@ Query:
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1067,35 +1242,6 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?)
Query:
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
FROM groups g
JOIN group_profiles gp USING (group_profile_id)
JOIN group_members mu USING (group_id)
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
WHERE g.user_id = ? AND mu.contact_id = ?
AND (LOWER(gp.display_name) LIKE '%' || ? || '%'
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
OR LOWER(gp.description) LIKE '%' || ? || '%'
)
Plan:
SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?)
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH pu USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT 1
FROM group_member_intros
@@ -1284,18 +1430,6 @@ Query:
Plan:
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT count (m.group_member_id)
FROM groups g
JOIN group_members m USING (group_id)
WHERE g.user_id = ?
AND g.group_id = ?
AND m.member_status NOT IN (?,?,?,?,?)
Plan:
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=?)
Query:
SELECT ct.contact_id
FROM contacts ct
@@ -1625,6 +1759,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO group_members
@@ -1634,6 +1793,31 @@ Query:
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?)
SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?)
SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?)
SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?)
SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?)
SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?)
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?)
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?)
SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?)
SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?)
SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?)
SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?)
SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?)
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
Query:
INSERT INTO msg_deliveries
@@ -4899,13 +5083,48 @@ Query:
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
WHERE g.user_id = ? AND mu.contact_id = ?
AND (LOWER(gp.display_name) LIKE '%' || ? || '%'
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
OR LOWER(gp.description) LIKE '%' || ? || '%'
)
Plan:
SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?)
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH pu USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id
@@ -4925,13 +5144,14 @@ Query:
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id
@@ -1,8 +1,7 @@
CREATE TABLE migrations(
name TEXT NOT NULL,
name TEXT NOT NULL PRIMARY KEY,
ts TEXT NOT NULL,
down TEXT,
PRIMARY KEY(name)
down TEXT
);
CREATE TABLE contact_profiles(
-- remote user profile
@@ -156,7 +155,8 @@ CREATE TABLE groups(
welcome_shared_msg_id BLOB,
request_shared_msg_id BLOB,
conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0,
via_group_link_uri BLOB, -- received
via_group_link_uri BLOB,
summary_current_members_count INTEGER NOT NULL DEFAULT 0, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -724,6 +724,10 @@ CREATE TABLE delivery_jobs(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE group_member_status_predicates(
member_status TEXT NOT NULL PRIMARY KEY,
current_member INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
full_name
@@ -1184,3 +1188,38 @@ CREATE INDEX idx_delivery_jobs_next ON delivery_jobs(
job_status
);
CREATE INDEX idx_delivery_jobs_created_at ON delivery_jobs(created_at);
CREATE INDEX idx_groups_summary_current_members_count ON groups(
summary_current_members_count
);
CREATE TRIGGER on_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END;
CREATE TRIGGER on_group_members_delete_update_summary
AFTER DELETE ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = OLD.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END;
CREATE TRIGGER on_group_members_update_update_summary
AFTER UPDATE ON group_members
FOR EACH ROW
WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = OLD.member_status AND current_member = 1)
!= EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
BEGIN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(
CASE WHEN EXISTS (SELECT 1 FROM group_member_status_predicates WHERE member_status = NEW.member_status AND current_member = 1)
THEN 1 ELSE -1 END
)
WHERE group_id = NEW.group_id;
END;
+25 -14
View File
@@ -37,7 +37,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), AConnShortLink (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode)
import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode)
import Simplex.Messaging.Agent.Store (AnyStoreError (..))
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
@@ -656,21 +656,22 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt,
type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow
type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime)
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. userMemberRow) =
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (uiThemes, currentMembers, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission}
businessChat = toBusinessChatInfo businessRow
preparedGroup = toPreparedGroup preparedGroupRow
in GroupInfo {groupId, useRelays = False, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention, viaGroupLinkUri}
groupSummary = GroupSummary {currentMembers}
in GroupInfo {groupId, useRelays = False, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri}
toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup
toPreparedGroup = \case
@@ -688,13 +689,14 @@ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer,
memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
supportChat = case supportChatTs_ of
Just chatTs ->
Just GroupSupportChat {
chatTs,
unread = supportChatUnread,
memberAttention = supportChatMemberAttention,
mentions = supportChatMentions,
lastMsgFromMemberTs = supportChatLastMsgFromMemberTs
}
Just
GroupSupportChat
{ chatTs,
unread = supportChatUnread,
memberAttention = supportChatMemberAttention,
mentions = supportChatMentions,
lastMsgFromMemberTs = supportChatLastMsgFromMemberTs
}
_ -> Nothing
in GroupMember {..}
@@ -707,7 +709,10 @@ toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just Busi
toBusinessChatInfo _ = Nothing
groupInfoQuery :: Query
groupInfoQuery =
groupInfoQuery = groupInfoQueryFields <> " " <> groupInfoQueryFrom
groupInfoQueryFields :: Query
groupInfoQueryFields =
[sql|
SELECT
-- GroupInfo
@@ -716,13 +721,18 @@ groupInfoQuery =
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts
|]
groupInfoQueryFrom :: Query
groupInfoQueryFrom =
[sql|
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id
@@ -808,7 +818,8 @@ setViaGroupLinkUri db groupId connId = do
DB.query
db
"SELECT via_contact_uri, via_contact_uri_hash FROM connections WHERE connection_id = ?"
(Only connId) :: IO [(Maybe ConnReqContact, Maybe ConnReqUriHash)]
(Only connId) ::
IO [(Maybe ConnReqContact, Maybe ConnReqUriHash)]
forM_ (listToMaybe r) $ \(viaContactUri, viaContactUriHash) ->
DB.execute
db
+8 -10
View File
@@ -487,6 +487,7 @@ data GroupInfo = GroupInfo
chatItemTTL :: Maybe Int64,
uiThemes :: Maybe UIThemeEntityOverrides,
customData :: Maybe CustomData,
groupSummary :: GroupSummary,
membersRequireAttention :: Int,
viaGroupLinkUri :: Maybe ConnReqContact
}
@@ -523,12 +524,9 @@ groupName' :: GroupInfo -> GroupName
groupName' GroupInfo {localDisplayName = g} = g
data GroupSummary = GroupSummary
{ currentMembers :: Int
{ currentMembers :: Int64
}
deriving (Show)
data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary, groupLink :: Maybe GroupLink}
deriving (Show)
deriving (Eq, Show)
data GroupLink = GroupLink
{ userContactLinkId :: Int64,
@@ -1240,7 +1238,9 @@ memberPending m = case memberStatus m of
memberCurrentOrPending :: GroupMember -> Bool
memberCurrentOrPending m = memberCurrent m || memberPending m
-- update getGroupSummary if this is changed
-- *** Please note:
-- *** update getGroupSummary and SQL function used in update triggers if this is changed
-- ***
memberCurrent' :: GroupMemberStatus -> Bool
memberCurrent' = \case
GSMemRejected -> False
@@ -2079,16 +2079,14 @@ $(JQ.deriveJSON defaultJSON ''BusinessChatInfo)
$(JQ.deriveJSON defaultJSON ''PreparedGroup)
$(JQ.deriveJSON defaultJSON ''GroupSummary)
$(JQ.deriveJSON defaultJSON ''GroupInfo)
$(JQ.deriveJSON defaultJSON ''Group)
$(JQ.deriveJSON defaultJSON ''GroupSummary)
$(JQ.deriveJSON defaultJSON ''GroupLink)
$(JQ.deriveJSON defaultJSON ''GroupInfoSummary)
instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
instance ToField MsgFilter where toField = toField . msgFilterInt
+7 -7
View File
@@ -128,7 +128,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
CRNetworkConfig netCfg -> viewNetworkConfig netCfg
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s
CRGroupInfo u g -> ttyUser u $ viewGroupInfo g
CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats
CRQueueInfo _ msgInfo qInfo ->
[ "last received msg: " <> maybe "none" viewJSON msgInfo,
@@ -1360,13 +1360,13 @@ viewContactConnected ct userIncognitoProfile testView =
Nothing ->
[ttyFullContact ct <> ": contact is connected"]
viewGroupsList :: [GroupInfoSummary] -> [StyledString]
viewGroupsList :: [GroupInfo] -> [StyledString]
viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <name>"]
viewGroupsList gs = map groupSS $ sortOn ldn_ gs
where
ldn_ :: GroupInfoSummary -> Text
ldn_ (GIS GroupInfo {localDisplayName} _ _) = T.toLower localDisplayName
groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers} _) =
ldn_ :: GroupInfo -> Text
ldn_ GroupInfo {localDisplayName} = T.toLower localDisplayName
groupSS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}, groupSummary = GroupSummary {currentMembers}} =
case memberStatus membership of
GSMemInvited -> groupInvitation' g
s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g
@@ -1628,8 +1628,8 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> viewUITheme uiThemes
<> viewCustomData customData
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId, uiThemes, customData} s =
viewGroupInfo :: GroupInfo -> [StyledString]
viewGroupInfo GroupInfo {groupId, uiThemes, customData, groupSummary = s} =
[ "group ID: " <> sShow groupId,
"current members: " <> sShow (currentMembers s)
]
+10 -7
View File
@@ -102,6 +102,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder =
profileNameLimit = maxBound,
captchaGenerator = Nothing,
directoryLog = Just $ ps </> "directory_service.log",
migrateDirectoryLog = Nothing,
serviceName = "SimpleX Directory",
runCLI = False,
searchResults = 3,
@@ -275,6 +276,7 @@ testSuspendResume ps =
bob <## "The group is listed in directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
superUser <## "The group remained listed in directory."
#if !defined(dbPostgres)
-- upgrade link
-- make it upgradeable first
superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
@@ -303,6 +305,7 @@ testSuspendResume ps =
superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there"
superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)"
bob <# "'SimpleX Directory'> hello there"
#endif
testDeleteGroup :: HasCallStack => TestParams -> IO ()
testDeleteGroup ps =
@@ -971,7 +974,7 @@ testDuplicateAskConfirmation ps =
cath #> "@'SimpleX Directory' /confirm 1:privacy"
welcomeWithLink <- groupAccepted cath "privacy"
groupNotFound bob "privacy"
completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2
completeRegistrationId superUser cath "privacy" "Privacy" welcomeWithLink 2 1
groupFound bob "privacy"
testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO ()
@@ -1029,10 +1032,10 @@ testDuplicateProhibitWhenUpdated ps =
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
cath ##> "/gp privacy security Security"
cath <## "changed to #security (Security)"
cath <# "'SimpleX Directory'> Thank you! The group link for ID 2 (security) is added to the welcome message."
cath <# "'SimpleX Directory'> Thank you! The group link for ID 1 (security) is added to the welcome message."
cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
approveRegistration superUser cath "security" 2
approveRegistrationId superUser cath "security" 2 1
groupFound bob "security"
groupFound cath "security"
@@ -1051,7 +1054,7 @@ testDuplicateProhibitApproval ps =
cath <# "'SimpleX Directory'> /confirm 1:privacy"
cath #> "@'SimpleX Directory' /confirm 1:privacy"
welcomeWithLink' <- groupAccepted cath "privacy"
updateProfileWithLink cath "privacy" welcomeWithLink' 2
updateProfileWithLink cath "privacy" welcomeWithLink' 1
notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2
groupNotFound cath "privacy"
completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
@@ -1116,7 +1119,7 @@ testListUserGroups promote ps =
checkListings ["security"] []
superUser #> "@'SimpleX Directory' /approve 1:privacy 1"
superUser <# "'SimpleX Directory'> > /approve 1:privacy 1"
superUser <## " Group approved!"
superUser <## " Group approved (promoted)!"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!"
bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
bob <## ""
@@ -1126,7 +1129,7 @@ testListUserGroups promote ps =
bob <## "/'link 1' - to view/upgrade group link."
checkListings ["privacy", "security"] ["privacy"]
checkListings :: [T.Text] -> [T.Text] -> IO ()
checkListings :: HasCallStack => [T.Text] -> [T.Text] -> IO ()
checkListings listed promoted = do
threadDelay 100000
checkListing listingFileName listed
@@ -1395,7 +1398,7 @@ withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do
runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
st <- restoreDirectoryStore directoryLog
st <- openDirectoryLog directoryLog
t <- forkIO $ directoryService st opts cfg
threadDelay 500000
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
+2 -1
View File
@@ -209,7 +209,8 @@ testCfg =
showReceipts = False,
shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"],
testView = True,
tbqSize = 16
tbqSize = 16,
confirmMigrations = MCYesUp
}
testCfgSlow :: ChatConfig
+3 -3
View File
@@ -48,14 +48,14 @@ postgresSchemaDumpTest migrations skipComparisonForDownMigrations testDBOpts@DBO
putStrLn $ "down migration " <> name m
let downMigr = fromJust $ toDownMigration m
schema <- getSchema testSchemaPath
Migrations.run st $ MTRUp [m]
Migrations.run st Nothing $ MTRUp [m]
schema' <- getSchema testSchemaPath
schema' `shouldNotBe` schema
Migrations.run st $ MTRDown [downMigr]
Migrations.run st Nothing $ MTRDown [downMigr]
unless (name m `elem` skipComparisonForDownMigrations) $ do
schema'' <- getSchema testSchemaPath
schema'' `shouldBe` schema
Migrations.run st $ MTRUp [m]
Migrations.run st Nothing $ MTRUp [m]
schema''' <- getSchema testSchemaPath
schema''' `shouldBe` schema'
+3 -3
View File
@@ -88,15 +88,15 @@ testSchemaMigrations = withTmpFiles $ do
putStrLn $ "down migration " <> name m
let downMigr = fromJust $ toDownMigration m
schema <- getSchema testDB testSchema
Migrations.run st True $ MTRUp [m]
Migrations.run st Nothing True $ MTRUp [m]
schema' <- getSchema testDB testSchema
unless (name m `elem` skipComparisonForUpMigrations) $
schema' `shouldNotBe` schema
Migrations.run st True $ MTRDown [downMigr]
Migrations.run st Nothing True $ MTRDown [downMigr]
unless (name m `elem` skipComparisonForDownMigrations) $ do
schema'' <- getSchema testDB testSchema
schema'' `shouldBe` schema
Migrations.run st True $ MTRUp [m]
Migrations.run st Nothing True $ MTRUp [m]
schema''' <- getSchema testDB testSchema
schema''' `shouldBe` schema'
+5 -6
View File
@@ -3,7 +3,6 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TupleSections #-}
import APIDocs
import Bots.BroadcastTests
import Bots.DirectoryTests
import ChatClient
@@ -28,8 +27,9 @@ import Control.Exception (bracket_)
import PostgresSchemaDump
import Simplex.Chat.Store.Postgres.Migrations (migrations)
import Simplex.Messaging.Agent.Store.Postgres.Util (createDBAndUserIfNotExists, dropAllSchemasExceptSystem, dropDatabaseAndUser)
import System.Directory (createDirectory, removePathForcibly)
import System.Directory (createDirectoryIfMissing, removePathForcibly)
#else
import APIDocs
import qualified Simplex.Messaging.TMap as TM
import MobileTests
import SchemaDump
@@ -45,12 +45,12 @@ main = do
#endif
withGlobalLogging logCfg . hspec
#if defined(dbPostgres)
. beforeAll_ (dropDatabaseAndUser testDBConnectInfo >> createDBAndUserIfNotExists testDBConnectInfo)
. afterAll_ (dropDatabaseAndUser testDBConnectInfo)
. before_ (dropDatabaseAndUser testDBConnectInfo >> createDBAndUserIfNotExists testDBConnectInfo)
. after_ (dropDatabaseAndUser testDBConnectInfo)
#endif
$ do
#if defined(dbPostgres)
around_ (bracket_ (createDirectory "tests/tmp") (removePathForcibly "tests/tmp")) $
around_ (bracket_ (createDirectoryIfMissing False "tests/tmp") (removePathForcibly "tests/tmp")) $
describe "Postgres schema dump" $
postgresSchemaDumpTest
migrations
@@ -72,7 +72,6 @@ main = do
describe "Random servers" randomServersTests
#if defined(dbPostgres)
around testBracket
. after_ (dropAllSchemasExceptSystem testDBConnectInfo)
#else
around (testBracket chatQueryStats agentQueryStats)
#endif