mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 12:22:23 +00:00
* core: chat relays protocol wip * types, notes * remove file * removal protocol * schema * status * update * recovery * update * formatting * rename * more types * comment * more docs * decrease number of steps * format * correct * update * update protocol * update * typo * todo * update doc * update * update * remove added * update * update * XGrpRelayReady * link to chat relays * update * remove from protocol * update * json * wip * remove comment * wip * update * wip * wip * update * wip * wip * plans * better view * fix * fix * relay acceptance * rework api * add relays to link * comment * active on con, fix send * comments * direct in group plan * prepare * member connection wip * comments * member connection wip * fix forwarding * introduce moderators to new member * enable relay tests * plans * security objectives * refactor * add to threat model * stress test wip * stress test wip * Revert "stress test wip" This reverts commitacde8a1fb3. * Revert "stress test wip" This reverts commit6435808438. * remove stress test * improve output * invert relay fkey * postgres schema * comments * group in progress, remove auto-select relays commented code * comments * corrections * comment * lint * redundant import * core: chat relay request worker (#6509) * update plans * strict tables * core: update group link asynchronously with relay link (#6548) * update simplexmq * docs: connection to chat relays rfc (#6554) * add test for 2 relays (doesn't pass) * create unknown member in same transaction as checking * fix relays choosing different memberId (XContactRelay) * plans, api * use same incognito profile for relays, connect concurrently, save correct link for plan * test * don't duplicate items on group connection * check relay record exists when joining * use mapConcurrently when adding relays, update schemas * fix multi-relay join for postgres (savepoint) * core: async retry connection to chat relays (#6584) * update simplexmq * fix api tests * prefer throwing temp error on connection * check group relays when deleting from configuration * relay_request_err_reason * relay role * rename, fix syntax * plans * rename, style --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
150 lines
6.9 KiB
Haskell
150 lines
6.9 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
|
|
module Directory.Store.Migrate where
|
|
|
|
import Control.Concurrent.STM
|
|
import Control.Monad
|
|
import Control.Monad.Except
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.List (find)
|
|
import Data.Maybe (fromMaybe)
|
|
import qualified Data.Text as T
|
|
import Directory.Listing
|
|
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.Store.Shared (getGroupInfo)
|
|
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 qualified Simplex.Messaging.TMap as TM
|
|
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
|
|
ctRegs <- TM.emptyIO
|
|
withActiveUser st $ \user -> withTransaction st $ \db -> do
|
|
forM_ gs $ \gr ->
|
|
whenM (verifyGroupRegistration db user gr) $ do
|
|
putStrLn $ "importing group " <> show (dbGroupId gr)
|
|
insertGroupReg db =<< fixUserGroupRegId ctRegs gr
|
|
renamePath logFile (logFile ++ ".bak")
|
|
putStrLn $ show (length gs) <> " group registrations imported"
|
|
where
|
|
fixUserGroupRegId ctRegs gr@GroupReg {dbGroupId, dbContactId} = do
|
|
ugIds <- fromMaybe [] <$> TM.lookupIO dbContactId ctRegs
|
|
gr' <-
|
|
if userGroupRegId gr `elem` ugIds
|
|
then do
|
|
let ugId = maximum ugIds + 1
|
|
putStrLn $ "Warning: updating userGroupRegId for group " <> show dbGroupId <> ", contact " <> show dbContactId
|
|
pure gr {userGroupRegId = ugId}
|
|
else pure gr
|
|
atomically $ TM.insert dbContactId (userGroupRegId gr' : ugIds) ctRegs
|
|
pure gr'
|
|
|
|
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) ->
|
|
whenM (verifyGroupRegistration db user gr) $
|
|
B.hPutStrLn h $ strEncode $ GRCreate gr
|
|
pure gs
|
|
putStrLn $ show (length gs) <> " group registrations exported"
|
|
|
|
saveGroupListingFiles :: DirectoryOpts -> ChatConfig -> IO ()
|
|
saveGroupListingFiles opts _cfg = case webFolder opts of
|
|
Nothing -> exit "use --web-folder to generate listings"
|
|
Just dir ->
|
|
withChatStore opts $ \st -> withActiveUser st $ \user ->
|
|
withTransaction st $ \db ->
|
|
getAllListedGroups_ db supportedChatVRange user >>= generateListing dir
|
|
|
|
verifyGroupRegistration :: DB.Connection -> User -> GroupReg -> IO Bool
|
|
verifyGroupRegistration db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} =
|
|
runExceptT (getGroupInfo db supportedChatVRange user gId) >>= \case
|
|
Left e -> False <$ putStrLn ("Error: loading group " <> show gId <> " (skipping): " <> show e)
|
|
Right GroupInfo {localDisplayName} -> do
|
|
let groupRef = show gId <> " " <> T.unpack localDisplayName
|
|
runExceptT (getHostMember db supportedChatVRange user gId) >>= \case
|
|
Left e -> False <$ putStrLn ("Error: loading host member of group " <> groupRef <> " (skipping): " <> show e)
|
|
Right GroupMember {groupMemberId = mId', memberContactId = ctId'} -> case dbOwnerMemberId of
|
|
Nothing -> True <$ putStrLn ("Warning: group " <> groupRef <> " has no owner member ID, host member ID is " <> show mId' <> ", registration status: " <> B.unpack (strEncode groupRegStatus))
|
|
Just mId
|
|
| mId /= mId' -> False <$ putStrLn ("Error: different host member ID of " <> groupRef <> " (skipping): " <> show mId')
|
|
| otherwise -> True <$ unless (Just ctId == ctId') (putStrLn $ "Warning: bad group " <> groupRef <> " 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
|