From 87e8a10f1e8aa0f46ead7cd768cfaa39e6d70e2f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 5 Jan 2026 08:53:26 +0000 Subject: [PATCH] core: use strict tables (#6535) * core: use strict tables * fix field types * change encodings to match schema types; migrate sqlite tables to strict mode * stabilize postgres client tests, remove slow handshake tests * update simplexmq * fix test * change call_state type to text * fix directory service queries * update local_alias for existing schemas * change types before strict --- .../src/Directory/Service.hs | 10 +- simplex-chat.cabal | 2 + src/Simplex/Chat/Controller.hs | 9 +- src/Simplex/Chat/Messages/CIContent.hs | 12 +-- src/Simplex/Chat/Store/AppSettings.hs | 7 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20251230_strict_tables.hs | 26 +++++ .../Store/Postgres/Migrations/chat_schema.sql | 2 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20251230_strict_tables.hs | 70 ++++++++++++ .../SQLite/Migrations/agent_query_plans.txt | 4 - .../Store/SQLite/Migrations/chat_schema.sql | 102 +++++++++--------- src/Simplex/Chat/Types.hs | 23 ++-- src/Simplex/Chat/Types/Preferences.hs | 2 +- src/Simplex/Chat/Types/Shared.hs | 33 +++--- src/Simplex/Chat/View.hs | 14 +-- tests/ChatTests/Groups.hs | 2 +- tests/ChatTests/Utils.hs | 6 +- tests/SchemaDump.hs | 9 +- 19 files changed, 220 insertions(+), 121 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 5901e1d61a..4be7b6177e 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -665,7 +665,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where rStatus = groupRolesStatus contactRole serviceRole groupRef = groupReference g - ctRole = "*" <> strEncodeTxt contactRole <> "*" + ctRole = "*" <> textEncode contactRole <> "*" suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () @@ -691,7 +691,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> pure () where groupRef = groupReference g - srvRole = "*" <> strEncodeTxt serviceRole <> "*" + srvRole = "*" <> textEncode serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = getOwnerGroupMember groupId gr @@ -801,7 +801,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n acceptMemberRole - <> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n") + <> ("Send /'role " <> tshow gId <> " " <> textEncode anotherRole <> "' to change it.\n\n") <> onlyViaLink gLink Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n Just mRole -> do @@ -809,7 +809,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." where - initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n" onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink DCGroupFilter gId gName_ acceptance_ -> (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do @@ -852,7 +852,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName T.unlines $ [ "The link to join the group " <> groupRef <> ":", groupLinkText gLink, - "New member role: " <> strEncodeTxt acceptMemberRole + "New member role: " <> textEncode acceptMemberRole ] <> ["The link is being upgraded..." | shouldBeUpgraded] when shouldBeUpgraded $ do diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 65bca0611f..95f386e9df 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -124,6 +124,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations + Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables else exposed-modules: Simplex.Chat.Archive @@ -271,6 +272,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations + Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 5754419933..30df251fbb 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -76,6 +76,7 @@ import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration) +import Simplex.Messaging.Agent.Store.DB (SQLError) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C @@ -96,13 +97,7 @@ import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM - -#if defined(dbPostgres) -import qualified Database.PostgreSQL.Simple as PSQL - -type SQLError = PSQL.SqlError -#else -import Database.SQLite.Simple (SQLError) +#if !defined(dbPostgres) import qualified Database.SQLite.Simple as SQL import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) #endif diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index fd8f1cc41b..fedb6cd8e0 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -22,7 +22,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Lazy as LB import Data.Int (Int64) import Data.Text (Text) -import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Text.Encoding (encodeUtf8) import Data.Type.Equality import Data.Word (Word32) import Simplex.Chat.Messages.CIContent.Events @@ -323,7 +323,7 @@ e2eInfoPQText = ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role = - "invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> (decodeLatin1 . strEncode $ role) + "invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> textEncode role rcvDirectEventToText :: RcvDirectEvent -> Text rcvDirectEventToText = \case @@ -338,9 +338,9 @@ rcvGroupEventToText = \case RGEMemberAccepted _ p -> "accepted " <> profileToText p RGEUserAccepted -> "accepted you" RGEMemberLeft -> "left" - RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p - RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r) + RGEUserRole r -> "changed your role to " <> textEncode r RGEMemberDeleted _ p -> "removed " <> profileToText p RGEUserDeleted -> "removed you" RGEGroupDeleted -> "deleted group" @@ -352,9 +352,9 @@ rcvGroupEventToText = \case sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case - SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r SGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p - SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r) + SGEUserRole r -> "changed role for yourself to " <> textEncode r SGEMemberDeleted _ p -> "removed " <> profileToText p SGEUserLeft -> "left" SGEGroupUpdated _ -> "group profile updated" diff --git a/src/Simplex/Chat/Store/AppSettings.hs b/src/Simplex/Chat/Store/AppSettings.hs index dbdd538cf4..4ee55b2143 100644 --- a/src/Simplex/Chat/Store/AppSettings.hs +++ b/src/Simplex/Chat/Store/AppSettings.hs @@ -5,11 +5,12 @@ module Simplex.Chat.Store.AppSettings where import Control.Monad (join) import Control.Monad.IO.Class (liftIO) -import qualified Data.Aeson as J import Data.Maybe (fromMaybe) import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings) import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (decodeJSON, encodeJSON) + #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..)) #else @@ -19,9 +20,9 @@ import Database.SQLite.Simple (Only (..)) saveAppSettings :: DB.Connection -> AppSettings -> IO () saveAppSettings db appSettings = do DB.execute_ db "DELETE FROM app_settings" - DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings) + DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ encodeJSON appSettings) getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings getAppSettings db platformDefaults = do - stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings") + stored_ <- join <$> liftIO (maybeFirstRow (decodeJSON . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings") pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 5fd2607f48..ef6d96bd45 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -23,6 +23,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations +import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -45,7 +46,8 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs new file mode 100644 index 0000000000..81bc789ceb --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables where + +import Data.Text (Text) +import Text.RawString.QQ (r) +import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables (isValidText) + +m20251230_strict_tables :: Text +m20251230_strict_tables = + isValidText + <> [r| +DELETE FROM calls +WHERE NOT simplex_is_valid_text(call_state); + +ALTER TABLE calls ALTER COLUMN call_state TYPE TEXT USING call_state::TEXT; + +DROP FUNCTION simplex_is_valid_text(BYTEA); +|] + +down_m20251230_strict_tables :: Text +down_m20251230_strict_tables = + [r| +ALTER TABLE calls ALTER COLUMN call_state TYPE BYTEA USING call_state::BYTEA; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index a3ca7693a6..238a6bfdbb 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -173,7 +173,7 @@ CREATE TABLE test_chat_schema.calls ( contact_id bigint NOT NULL, shared_call_id bytea NOT NULL, chat_item_id bigint NOT NULL, - call_state bytea NOT NULL, + call_state text NOT NULL, call_ts timestamp with time zone NOT NULL, user_id bigint NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 2e5866beae..f740c5654f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -146,6 +146,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations +import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -291,7 +292,8 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs new file mode 100644 index 0000000000..be92da8f5e --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20251230_strict_tables :: Query +m20251230_strict_tables = + [sql| +UPDATE group_members +SET member_role = CAST(member_role as TEXT), + member_restriction = CAST(member_restriction as TEXT); + +UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as TEXT); + +UPDATE app_settings SET app_settings = CAST(app_settings as TEXT); + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'call_state BLOB NOT NULL', 'call_state TEXT NOT NULL') +WHERE type = 'table' AND name = 'calls'; + +UPDATE sqlite_master +SET sql = replace(sql, 'local_alias DEFAULT', 'local_alias TEXT DEFAULT') +WHERE type = 'table' AND name = 'connections'; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -15)) = ') without rowid' THEN sql || ', STRICT' + WHEN SUBSTR(sql, -1) = ')' THEN sql || ' STRICT' + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +PRAGMA writable_schema=0; +|] + +down_m20251230_strict_tables :: Query +down_m20251230_strict_tables = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -8)) = ', strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 8) + WHEN LOWER(SUBSTR(sql, -7)) = ' strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 7) + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +UPDATE sqlite_master +SET sql = replace(sql, 'call_state TEXT NOT NULL', 'call_state BLOB NOT NULL') +WHERE type = 'table' AND name = 'calls'; + +UPDATE sqlite_master +SET sql = replace(sql, 'local_alias TEXT DEFAULT', 'local_alias DEFAULT') +WHERE type = 'table' AND name = 'connections'; + +PRAGMA writable_schema=0; + +UPDATE group_members +SET member_role = CAST(member_role as BLOB), + member_restriction = CAST(member_restriction as BLOB); + +UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as BLOB); + +UPDATE app_settings SET app_settings = CAST(app_settings as BLOB); +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index efbadf7c53..6d3f69d5fa 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -1255,10 +1255,6 @@ Query: UPDATE snd_file_chunk_replicas SET replica_status = ?, updated_at = ? WHE Plan: SEARCH snd_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE snd_files SET deleted = 1, updated_at = ? WHERE snd_file_id = ? -Plan: -SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE snd_files SET prefix_path = NULL, status = ?, updated_at = ? WHERE snd_file_id = ? Plan: SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 1e2db113c3..fd4fa914d0 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -2,7 +2,7 @@ CREATE TABLE migrations( name TEXT NOT NULL PRIMARY KEY, ts TEXT NOT NULL, down TEXT -); +) STRICT; CREATE TABLE contact_profiles( -- remote user profile contact_profile_id INTEGER PRIMARY KEY, @@ -20,7 +20,7 @@ CREATE TABLE contact_profiles( contact_link BLOB, short_descr TEXT, chat_peer_type TEXT -); +) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE RESTRICT @@ -44,7 +44,7 @@ CREATE TABLE users( ON DELETE RESTRICT ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED -); +) STRICT; CREATE TABLE display_names( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, @@ -54,7 +54,7 @@ CREATE TABLE display_names( updated_at TEXT CHECK(updated_at NOT NULL), PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL, UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE contacts( contact_id INTEGER PRIMARY KEY, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, @@ -96,7 +96,7 @@ CREATE TABLE contacts( ON UPDATE CASCADE, UNIQUE(user_id, local_display_name), UNIQUE(user_id, contact_profile_id) -); +) STRICT; CREATE TABLE known_servers( server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, @@ -106,7 +106,7 @@ CREATE TABLE known_servers( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), UNIQUE(user_id, host, port) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE group_profiles( -- shared group profiles group_profile_id INTEGER PRIMARY KEY, @@ -122,7 +122,7 @@ CREATE TABLE group_profiles( description TEXT NULL, member_admission TEXT, short_descr TEXT -); +) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -163,7 +163,7 @@ CREATE TABLE groups( ON UPDATE CASCADE, UNIQUE(user_id, local_display_name), UNIQUE(user_id, group_profile_id) -); +) STRICT; CREATE TABLE group_members( -- group members, excluding the local user group_member_id INTEGER PRIMARY KEY, @@ -203,7 +203,7 @@ CREATE TABLE group_members( ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE(group_id, member_id) -); +) STRICT; CREATE TABLE group_member_intros( group_member_intro_id INTEGER PRIMARY KEY, re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, @@ -215,7 +215,7 @@ CREATE TABLE group_member_intros( updated_at TEXT CHECK(updated_at NOT NULL), intro_chat_protocol_version INTEGER NOT NULL DEFAULT 3, -- see GroupMemberIntroStatus UNIQUE(re_group_member_id, to_group_member_id) -); +) STRICT; CREATE TABLE files( file_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -240,7 +240,7 @@ CREATE TABLE files( file_crypto_nonce BLOB, note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE -); +) STRICT; CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, @@ -253,7 +253,7 @@ CREATE TABLE snd_files( file_descr_id INTEGER NULL REFERENCES xftp_file_descriptions ON DELETE SET NULL, PRIMARY KEY(file_id, connection_id) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE rcv_files( file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed @@ -270,7 +270,7 @@ CREATE TABLE rcv_files( agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), to_receive INTEGER, user_approved_relays INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE rcv_file_chunks( file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE, chunk_number INTEGER NOT NULL, @@ -279,7 +279,7 @@ CREATE TABLE rcv_file_chunks( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file) PRIMARY KEY(file_id, chunk_number) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE connections( -- all SMP agent connections connection_id INTEGER PRIMARY KEY, @@ -302,7 +302,7 @@ CREATE TABLE connections( REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL, custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, conn_req_inv BLOB, - local_alias DEFAULT '' CHECK(local_alias NOT NULL), + local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL), via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL), group_link_id BLOB, security_code TEXT NULL, @@ -325,7 +325,7 @@ CREATE TABLE connections( REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED -); +) STRICT; CREATE TABLE user_contact_links( user_contact_link_id INTEGER PRIMARY KEY, conn_req_contact BLOB NOT NULL, @@ -344,7 +344,7 @@ CREATE TABLE user_contact_links( short_link_data_set INTEGER NOT NULL DEFAULT 0, short_link_large_data_set INTEGER NOT NULL DEFAULT 0, UNIQUE(user_id, local_display_name) -); +) STRICT; CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, user_contact_link_id INTEGER REFERENCES user_contact_links @@ -372,7 +372,7 @@ CREATE TABLE contact_requests( DEFERRABLE INITIALLY DEFERRED, UNIQUE(user_id, local_display_name), UNIQUE(user_id, contact_profile_id) -); +) STRICT; CREATE TABLE messages( message_id INTEGER PRIMARY KEY, msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent @@ -388,14 +388,14 @@ CREATE TABLE messages( author_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, broker_ts TEXT -); +) STRICT; CREATE TABLE pending_group_messages( pending_group_message_id INTEGER PRIMARY KEY, group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_items( chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -441,7 +441,7 @@ CREATE TABLE chat_items( group_scope_tag TEXT, group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, show_group_as_sender INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -449,21 +449,21 @@ CREATE TABLE chat_item_messages( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), UNIQUE(chat_item_id, message_id) -); +) STRICT; CREATE TABLE calls( -- stores call invitations state for communicating state between NSE and app when call notification comes call_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, shared_call_id BLOB NOT NULL, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, - call_state BLOB NOT NULL, + call_state TEXT NOT NULL, call_ts TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) , call_uuid TEXT NOT NULL DEFAULT "" -); +) STRICT; CREATE TABLE commands( command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId connection_id INTEGER REFERENCES connections ON DELETE CASCADE, @@ -472,14 +472,14 @@ CREATE TABLE commands( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE settings( settings_id INTEGER PRIMARY KEY, chat_item_ttl INTEGER, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE IF NOT EXISTS "protocol_servers"( smp_server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, @@ -494,7 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', UNIQUE(user_id, host, port) -); +) STRICT; CREATE TABLE xftp_file_descriptions( file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -503,7 +503,7 @@ CREATE TABLE xftp_file_descriptions( file_descr_complete INTEGER NOT NULL DEFAULT(0), created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE extra_xftp_file_descriptions( extra_file_descr_id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -511,7 +511,7 @@ CREATE TABLE extra_xftp_file_descriptions( file_descr_text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_versions( -- contains versions only for edited chat items, including current version chat_item_version_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -520,7 +520,7 @@ CREATE TABLE chat_item_versions( item_version_ts TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_reactions( chat_item_reaction_id INTEGER PRIMARY KEY AUTOINCREMENT, item_member_id BLOB, -- member that created item, NULL for items in direct chats @@ -534,7 +534,7 @@ CREATE TABLE chat_item_reactions( reaction_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_moderations( chat_item_moderation_id INTEGER PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -545,7 +545,7 @@ CREATE TABLE chat_item_moderations( moderated_at TEXT NOT NULL, -- broker_ts of creating message created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE group_snd_item_statuses( group_snd_item_status_id INTEGER PRIMARY KEY, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -555,7 +555,7 @@ CREATE TABLE group_snd_item_statuses( updated_at TEXT NOT NULL DEFAULT(datetime('now')) , via_proxy INTEGER -); +) STRICT; CREATE TABLE IF NOT EXISTS "sent_probes"( sent_probe_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -565,7 +565,7 @@ CREATE TABLE IF NOT EXISTS "sent_probes"( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), UNIQUE(user_id, probe) -); +) STRICT; CREATE TABLE IF NOT EXISTS "sent_probe_hashes"( sent_probe_hash_id INTEGER PRIMARY KEY, sent_probe_id INTEGER NOT NULL REFERENCES "sent_probes" ON DELETE CASCADE, @@ -574,7 +574,7 @@ CREATE TABLE IF NOT EXISTS "sent_probe_hashes"( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL) -); +) STRICT; CREATE TABLE IF NOT EXISTS "received_probes"( received_probe_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -584,7 +584,7 @@ CREATE TABLE IF NOT EXISTS "received_probes"( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL) -); +) STRICT; CREATE TABLE remote_hosts( -- e.g., mobiles known to a desktop app remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -599,7 +599,7 @@ CREATE TABLE remote_hosts( bind_addr TEXT, bind_iface TEXT, bind_port INTEGER -); +) STRICT; CREATE TABLE remote_controllers( -- e.g., desktops known to a mobile app remote_ctrl_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -610,7 +610,7 @@ CREATE TABLE remote_controllers( id_pub BLOB NOT NULL, -- remote controller long-term/identity key to verify signatures dh_priv_key BLOB NOT NULL, -- last session DH key prev_dh_priv_key BLOB -- previous session DH key -); +) STRICT; CREATE TABLE IF NOT EXISTS "msg_deliveries"( msg_delivery_id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages and for batched messages @@ -621,7 +621,7 @@ CREATE TABLE IF NOT EXISTS "msg_deliveries"( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), delivery_status TEXT -- MsgDeliveryStatus -); +) STRICT; CREATE TABLE note_folders( note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -630,8 +630,8 @@ CREATE TABLE note_folders( chat_ts TEXT NOT NULL DEFAULT(datetime('now')), favorite INTEGER NOT NULL DEFAULT 0, unread_chat INTEGER NOT NULL DEFAULT 0 -); -CREATE TABLE app_settings(app_settings TEXT NOT NULL); +) STRICT; +CREATE TABLE app_settings(app_settings TEXT NOT NULL) STRICT; CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, @@ -645,14 +645,14 @@ CREATE TABLE server_operators( xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE usage_conditions( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, conditions_commit TEXT NOT NULL UNIQUE, notified_at TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE operator_usage_conditions( operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, @@ -662,26 +662,26 @@ CREATE TABLE operator_usage_conditions( created_at TEXT NOT NULL DEFAULT(datetime('now')) , auto_accepted INTEGER DEFAULT 0 -); +) STRICT; CREATE TABLE chat_tags( chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users ON DELETE CASCADE, chat_tag_text TEXT NOT NULL, chat_tag_emoji TEXT, tag_order INTEGER NOT NULL -); +) STRICT; CREATE TABLE chat_tags_chats( contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE -); +) STRICT; CREATE TABLE chat_item_mentions( chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, member_id BLOB NOT NULL, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, display_name TEXT NOT NULL -); +) STRICT; CREATE TABLE delivery_tasks( delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -697,7 +697,7 @@ CREATE TABLE delivery_tasks( failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE delivery_jobs( delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -713,16 +713,16 @@ CREATE TABLE delivery_jobs( failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE group_member_status_predicates( member_status TEXT NOT NULL PRIMARY KEY, current_member INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE connections_sync( connections_sync_id INTEGER PRIMARY KEY AUTOINCREMENT, should_sync INTEGER NOT NULL DEFAULT 0, last_sync_ts TEXT -); +) STRICT; CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 7aba1c2ad5..44ea46492c 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -58,7 +58,7 @@ import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal #if defined(dbPostgres) @@ -871,27 +871,26 @@ data MemberRestrictionStatus | MRSUnknown Text deriving (Eq, Show) -instance FromField MemberRestrictionStatus where fromField = blobFieldDecoder strDecode +instance FromField MemberRestrictionStatus where fromField = fromTextField_ textDecode -instance ToField MemberRestrictionStatus where toField = toField . strEncode +instance ToField MemberRestrictionStatus where toField = toField . textEncode -instance StrEncoding MemberRestrictionStatus where - strEncode = \case +instance TextEncoding MemberRestrictionStatus where + textEncode = \case MRSBlocked -> "blocked" MRSUnrestricted -> "unrestricted" - MRSUnknown tag -> encodeUtf8 tag - strDecode s = Right $ case s of + MRSUnknown tag -> tag + textDecode s = Just $ case s of "blocked" -> MRSBlocked "unrestricted" -> MRSUnrestricted - tag -> MRSUnknown $ safeDecodeUtf8 tag - strP = strDecode <$?> A.takeByteString + tag -> MRSUnknown tag instance FromJSON MemberRestrictionStatus where - parseJSON = strParseJSON "MemberRestrictionStatus" + parseJSON = textParseJSON "MemberRestrictionStatus" instance ToJSON MemberRestrictionStatus where - toJSON = strToJSON - toEncoding = strToJEncoding + toJSON = textToJSON + toEncoding = textToEncoding mrsBlocked :: MemberRestrictionStatus -> Bool mrsBlocked = \case diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index f85e910bf8..d8c6f10b3a 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -782,7 +782,7 @@ groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> groupPrefStateText feature pref param role = let enabled = getField @"enable" pref paramText = if enabled == FEOn then groupParamText_ feature param else "" - roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role + roleText = maybe "" (\r -> " for " <> textEncode r <> "s") role in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText groupParamText_ :: GroupFeature -> Maybe Int -> Text diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 60ebe9d033..280fc32ea4 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -7,7 +7,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import Simplex.Chat.Options.DB (FromField (..), ToField (..)) -import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$?>)) @@ -20,34 +20,33 @@ data GroupMemberRole | GROwner -- + delete and change group information, add/remove/change roles for Owners deriving (Eq, Show, Ord) -instance FromField GroupMemberRole where fromField = blobFieldDecoder strDecode +instance FromField GroupMemberRole where fromField = fromTextField_ textDecode -instance ToField GroupMemberRole where toField = toField . strEncode +instance ToField GroupMemberRole where toField = toField . textEncode -instance StrEncoding GroupMemberRole where - strEncode = \case +instance TextEncoding GroupMemberRole where + textEncode = \case GROwner -> "owner" GRAdmin -> "admin" GRModerator -> "moderator" GRMember -> "member" GRAuthor -> "author" GRObserver -> "observer" - strDecode = \case - "owner" -> Right GROwner - "admin" -> Right GRAdmin - "moderator" -> Right GRModerator - "member" -> Right GRMember - "author" -> Right GRAuthor - "observer" -> Right GRObserver - r -> Left $ "bad GroupMemberRole " <> B.unpack r - strP = strDecode <$?> A.takeByteString + textDecode = \case + "owner" -> Just GROwner + "admin" -> Just GRAdmin + "moderator" -> Just GRModerator + "member" -> Just GRMember + "author" -> Just GRAuthor + "observer" -> Just GRObserver + r -> Nothing instance FromJSON GroupMemberRole where - parseJSON = strParseJSON "GroupMemberRole" + parseJSON = textParseJSON "GroupMemberRole" instance ToJSON GroupMemberRole where - toJSON = strToJSON - toEncoding = strToJEncoding + toJSON = textToJSON + toEncoding = textToEncoding data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a8526e357f..b515123928 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1231,7 +1231,7 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] viewReceivedGroupInvitation g c role = - ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) + ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role : case incognitoMembershipProfile g of Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)] Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"] @@ -1270,15 +1270,15 @@ viewMembersBlockedForAllUser g members blocked = case members of mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"] showRole :: GroupMemberRole -> StyledString -showRole = plain . strEncode +showRole = plain . textEncode viewGroupMembers :: Group -> [StyledString] viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members where removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft - groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m) - role :: GroupMember -> String - role GroupMember {memberRole} = B.unpack $ strEncode memberRole + groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (T.intercalate ", " $ [role m] <> category m <> status m <> muted m) + role :: GroupMember -> Text + role GroupMember {memberRole} = textEncode memberRole category m = case memberCategory m of GCUserMember -> ["you"] GCInviteeMember -> ["invited"] @@ -2455,8 +2455,8 @@ viewChatError isCmd logLevel testView = \case CEGroupUserRole g role -> (: []) . (ttyGroup' g <>) $ case role of GRAuthor -> ": you don't have permission to send messages" - _ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role) - CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"] + _ -> ": you have insufficient permissions for this action, the required role is " <> showRole role + CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> showRole role <> ", use member or observer"] CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"] CEGroupIncognitoCantInvite -> ["you are using an incognito profile for this group - prohibited to invite contacts"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 166528e69c..121d5a92f8 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -8389,7 +8389,7 @@ testChannelsRelayDeliver = createChannel5 :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO () createChannel5 alice bob cath dan eve mRole = do createGroup2 "team" alice bob - bob ##> ("/create link #team " <> B.unpack (strEncode mRole)) + bob ##> ("/create link #team " <> T.unpack (textEncode mRole)) gLink <- getGroupLink bob "team" mRole True cath ##> ("/c " <> gLink) cath <## "connection request sent!" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 0e99b26bc5..dded39e692 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -593,7 +593,7 @@ getGroupLink_ cc gName mRole created = do cc <## "" link <- getTermLine cc cc <## "" - cc <## ("Anybody can connect to you and join group as " <> B.unpack (strEncode mRole) <> " with: /c ") + cc <## ("Anybody can connect to you and join group as " <> T.unpack (textEncode mRole) <> " with: /c ") cc <## ("to show it again: /show link #" <> gName) cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)") pure link @@ -809,12 +809,12 @@ fullAddMember :: HasCallStack => String -> String -> TestCC -> TestCC -> GroupMe fullAddMember gName fullName inviting invitee role = do name1 <- userName inviting memName <- userName invitee - inviting ##> ("/a " <> gName <> " " <> memName <> " " <> B.unpack (strEncode role)) + inviting ##> ("/a " <> gName <> " " <> memName <> " " <> T.unpack (textEncode role)) let fullName' = if null fullName || fullName == gName then "" else " (" <> fullName <> ")" concurrentlyN_ [ inviting <## ("invitation to join the group #" <> gName <> " sent to " <> memName), do - invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role)) + invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> T.unpack (textEncode role)) invitee <## ("use /j " <> gName <> " to accept") ] diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 5646031cc2..2c4ba05ce6 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -17,7 +17,7 @@ import Data.Maybe (fromJust, isJust) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T -import Database.SQLite.Simple (Query (..)) +import Database.SQLite.Simple (Only (..), Query (..)) import Simplex.Chat.Options.SQLite (chatDBFunctions) import Simplex.Chat.Store (createChatStore) import qualified Simplex.Chat.Store as Store @@ -59,6 +59,7 @@ schemaDumpTest = do it "verify and overwrite schema dump" testVerifySchemaDump it "verify .lint fkey-indexes" testVerifyLintFKeyIndexes it "verify schema down migrations" testSchemaMigrations + it "verify strict tables" testVerifyStrict testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do @@ -101,6 +102,12 @@ testSchemaMigrations = withTmpFiles $ do schema''' <- getSchema testDB testSchema schema''' `shouldBe` schema' +testVerifyStrict :: IO () +testVerifyStrict = do + Right st <- createDBStore (DBOpts testDB chatDBFunctions "" False True TQOff) Store.migrations (MigrationConfig MCConsole Nothing) + withConnection st (`DB.query_` "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND sql NOT LIKE '% STRICT'") + `shouldReturn` ([] :: [Only Text]) + skipComparisonForUpMigrations :: [String] skipComparisonForUpMigrations = [ -- schema doesn't change