mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-07 01:50:05 +00:00
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:
@@ -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_
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -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
@@ -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,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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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
@@ -209,7 +209,8 @@ testCfg =
|
||||
showReceipts = False,
|
||||
shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"],
|
||||
testView = True,
|
||||
tbqSize = 16
|
||||
tbqSize = 16,
|
||||
confirmMigrations = MCYesUp
|
||||
}
|
||||
|
||||
testCfgSlow :: ChatConfig
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user