From 41c9742b0d2087d73b5e6b05cbe85f03935a1337 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Jun 2022 19:41:25 +0100 Subject: [PATCH] core: auto-reply message for user contact addresses (#755) * core: auto-reply message for user contact addresses * terminal: show auto accept status and message * test --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 25 +- src/Simplex/Chat/Bot.hs | 2 +- src/Simplex/Chat/Controller.hs | 6 +- .../Chat/Migrations/M20220626_auto_reply.hs | 15 + src/Simplex/Chat/Migrations/chat_schema.sql | 376 +++++++++++------- src/Simplex/Chat/Store.hs | 104 +++-- src/Simplex/Chat/Types.hs | 3 +- src/Simplex/Chat/Util.hs | 3 + src/Simplex/Chat/View.hs | 9 +- tests/ChatTests.hs | 24 ++ tests/SchemaDump.hs | 2 +- 12 files changed, 366 insertions(+), 204 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220626_auto_reply.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 61b0e1f5ae..e2eedcecf9 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -39,6 +39,7 @@ library Simplex.Chat.Migrations.M20220321_chat_item_edited Simplex.Chat.Migrations.M20220404_files_status_fields Simplex.Chat.Migrations.M20220514_profiles_user_id + Simplex.Chat.Migrations.M20220626_auto_reply Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.Protocol diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6db48345cd..8a99e2deb5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -52,7 +52,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Types -import Simplex.Chat.Util (safeDecodeUtf8) +import Simplex.Chat.Util (safeDecodeUtf8, uncurry3) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig) import Simplex.Messaging.Agent.Protocol @@ -592,9 +592,9 @@ processChatCommand = \case withStore' (`deleteUserContactLink` userId) pure CRUserContactLinkDeleted ShowMyAddress -> withUser $ \User {userId} -> - uncurry CRUserContactLink <$> withStore (`getUserContactLink` userId) - AddressAutoAccept onOff -> withUser $ \User {userId} -> do - uncurry CRUserContactLinkUpdated <$> withStore (\db -> updateUserContactLinkAutoAccept db userId onOff) + uncurry3 CRUserContactLink <$> withStore (`getUserContactLink` userId) + AddressAutoAccept onOff msgContent -> withUser $ \User {userId} -> do + uncurry3 CRUserContactLinkUpdated <$> withStore (\db -> updateUserContactLinkAutoAccept db userId onOff msgContent) AcceptContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIAcceptContact connReqId @@ -996,9 +996,9 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> m Contact -acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, xContactId} = do +acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} = do connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile - withStore' $ \db -> createAcceptedContact db userId connId cName profileId p xContactId + withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () agentSubscriber user subConns = do @@ -1136,7 +1136,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> Nothing processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg conn@Connection {connId} = \case + processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink} = \case Nothing -> case agentMsg of CONF confId connInfo -> do saveConnInfo conn connInfo @@ -1205,6 +1205,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView $ CRContactConnected ct setActive $ ActiveC c showToast (c <> "> ") "connected" + forM_ viaUserContactLink $ \userContactLinkId -> do + withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case + Just (_, True, Just mc) -> do + msg <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) Nothing Nothing + toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci + _ -> pure () Just (gInfo, m@GroupMember {activeConn}) -> do when (maybe False ((== ConnReady) . connStatus) activeConn) $ do notifyMemberConnected gInfo m @@ -1438,7 +1445,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage withStore (\db -> createOrUpdateContactRequest db userId userContactLinkId invId p xContactId_) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact CORRequest cReq@UserContactRequest {localDisplayName} -> do - (_, autoAccept) <- withStore $ \db -> getUserContactLink db userId + (_, autoAccept, _) <- withStore $ \db -> getUserContactLink db userId if autoAccept then acceptContactRequest user cReq >>= toView . CRAcceptingContactRequest else do @@ -2325,7 +2332,7 @@ chatCommandP = <|> ("/address" <|> "/ad") $> CreateMyAddress <|> ("/delete_address" <|> "/da") $> DeleteMyAddress <|> ("/show_address" <|> "/sa") $> ShowMyAddress - <|> "/auto_accept " *> (AddressAutoAccept <$> onOffP) + <|> "/auto_accept " *> (AddressAutoAccept <$> onOffP <*> optional (A.space *> msgContentP)) <|> ("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName) <|> ("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName) <|> ("/markdown" <|> "/m") $> ChatHelp HSMarkdown diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 83a8be168e..ef055143a6 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -38,7 +38,7 @@ chatBotRepl welcome answer _user cc = do initializeBotAddress :: ChatController -> IO () initializeBotAddress cc = do sendChatCmd cc "/show_address" >>= \case - CRUserContactLink uri _ -> showBotAddress uri + CRUserContactLink uri _ _ -> showBotAddress uri CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do putStrLn $ "No bot address, creating..." sendChatCmd cc "/address" >>= \case diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 67ab50029d..0b12fb7bae 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -147,7 +147,7 @@ data ChatCommand | CreateMyAddress | DeleteMyAddress | ShowMyAddress - | AddressAutoAccept Bool + | AddressAutoAccept Bool (Maybe MsgContent) | AcceptContact ContactName | RejectContact ContactName | SendMessage ChatName ByteString @@ -206,8 +206,8 @@ data ChatResponse | CRGroupCreated {groupInfo :: GroupInfo} | CRGroupMembers {group :: Group} | CRContactsList {contacts :: [Contact]} - | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool} - | CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool} + | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} + | CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} | CRContactRequestRejected {contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {groupInfo :: GroupInfo} | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} diff --git a/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs b/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs new file mode 100644 index 0000000000..6ac72ac804 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220626_auto_reply where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220626_auto_reply :: Query +m20220626_auto_reply = + [sql| +ALTER TABLE user_contact_links ADD COLUMN auto_reply_msg_content TEXT DEFAULT NULL; + +ALTER TABLE connections ADD COLUMN via_user_contact_link INTEGER DEFAULT NULL + REFERENCES user_contact_links (user_contact_link_id) ON DELETE SET NULL; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f3fa1f3b48..55429b04a5 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -1,130 +1,167 @@ -CREATE TABLE migrations ( - name TEXT NOT NULL, - ts TEXT NOT NULL, - PRIMARY KEY (name) - ); -CREATE TABLE contact_profiles ( -- remote user profile +CREATE TABLE migrations( + name TEXT NOT NULL, + ts TEXT NOT NULL, + PRIMARY KEY(name) +); +CREATE TABLE contact_profiles( + -- remote user profile contact_profile_id INTEGER PRIMARY KEY, - display_name TEXT NOT NULL, -- contact name set by remote user (not unique), this name must not contain spaces + display_name TEXT NOT NULL, -- contact name set by remote user(not unique), this name must not contain spaces full_name TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}' -- JSON with contact profile properties -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE); -CREATE INDEX contact_profiles_index ON contact_profiles (display_name, full_name); -CREATE TABLE users ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + image TEXT, + user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE +); +CREATE INDEX contact_profiles_index ON contact_profiles( + display_name, + full_name +); +CREATE TABLE users( user_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED, + DEFERRABLE INITIALLY DEFERRED, local_display_name TEXT NOT NULL UNIQUE, - active_user INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 1 for active user - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE - DEFERRABLE INITIALLY DEFERRED + active_user INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- 1 for active user + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE + DEFERRABLE INITIALLY DEFERRED ); -CREATE TABLE display_names ( +CREATE TABLE display_names( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, ldn_base TEXT NOT NULL, - ldn_suffix INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), 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 + ldn_suffix INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + 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; -CREATE TABLE contacts ( +CREATE TABLE contacts( contact_id INTEGER PRIMARY KEY, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - local_display_name TEXT NOT NULL, - is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user - via_group INTEGER REFERENCES groups (group_id) ON DELETE SET NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB, - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, contact_profile_id) +user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, +local_display_name TEXT NOT NULL, +is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user + via_group INTEGER REFERENCES groups(group_id) ON DELETE SET NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT CHECK(updated_at NOT NULL), + xcontact_id BLOB, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, contact_profile_id) ); -CREATE TABLE sent_probes ( +CREATE TABLE sent_probes( sent_probe_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE, probe BLOB NOT NULL, - 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), - UNIQUE (user_id, probe) + 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), + UNIQUE(user_id, probe) ); -CREATE TABLE sent_probe_hashes ( +CREATE TABLE sent_probe_hashes( sent_probe_hash_id INTEGER PRIMARY KEY, sent_probe_id INTEGER NOT NULL REFERENCES sent_probes ON DELETE CASCADE, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, - 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), - UNIQUE (sent_probe_id, contact_id) + 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), + UNIQUE(sent_probe_id, contact_id) ); -CREATE TABLE received_probes ( +CREATE TABLE received_probes( received_probe_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, probe BLOB, probe_hash BLOB NOT NULL, 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)); + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); CREATE TABLE known_servers( server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, port TEXT NOT NULL, key_hash BLOB, - 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), - UNIQUE (user_id, host, port) + 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), + UNIQUE(user_id, host, port) ) WITHOUT ROWID; -CREATE TABLE group_profiles ( -- shared group profiles +CREATE TABLE group_profiles( + -- shared group profiles group_profile_id INTEGER PRIMARY KEY, display_name TEXT NOT NULL, -- this name must not contain spaces full_name TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}' -- JSON with user or contact profile -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE); -CREATE TABLE groups ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + image TEXT, + user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE +); +CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, -- local group name without spaces group_profile_id INTEGER REFERENCES group_profiles ON DELETE SET NULL, -- shared group profile - inv_queue_info BLOB, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- received - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, group_profile_id) + inv_queue_info BLOB, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- received + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, group_profile_id) ); -CREATE INDEX idx_groups_inv_queue_info ON groups (inv_queue_info); -CREATE TABLE group_members ( -- group members, excluding the local user +CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info); +CREATE TABLE group_members( + -- group members, excluding the local user group_member_id INTEGER PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, member_id BLOB NOT NULL, -- shared member ID, unique per group member_role TEXT NOT NULL, -- owner, admin, member member_category TEXT NOT NULL, -- see GroupMemberCategory member_status TEXT NOT NULL, -- see GroupMemberStatus - invited_by INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator + invited_by INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator sent_inv_queue_info BLOB, -- sent group_queue_info BLOB, -- received direct_queue_info BLOB, -- received user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, -- should be the same as contact contact_profile_id INTEGER NOT NULL REFERENCES contact_profiles ON DELETE CASCADE, - contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (group_id, member_id) + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(group_id, member_id) ); -CREATE TABLE group_member_intros ( +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, - to_group_member_id INTEGER NOT NULL REFERENCES group_members (group_member_id) ON DELETE CASCADE, + re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + to_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, group_queue_info BLOB, direct_queue_info BLOB, - intro_status TEXT NOT NULL, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- see GroupMemberIntroStatus - UNIQUE (re_group_member_id, to_group_member_id) + intro_status TEXT NOT NULL, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- see GroupMemberIntroStatus + UNIQUE(re_group_member_id, to_group_member_id) ); -CREATE TABLE files ( +CREATE TABLE files( file_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, @@ -132,152 +169,203 @@ CREATE TABLE files ( file_path TEXT, file_size INTEGER NOT NULL, chunk_size INTEGER NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT(datetime('now')), user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE -, chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), cancelled INTEGER, ci_file_status TEXT); -CREATE TABLE snd_files ( + , + chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + cancelled INTEGER, + ci_file_status TEXT +); +CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed - group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - PRIMARY KEY (file_id, connection_id) + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + PRIMARY KEY(file_id, connection_id) ) WITHOUT ROWID; -CREATE TABLE rcv_files ( +CREATE TABLE rcv_files( file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, file_queue_info BLOB -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL)); -CREATE TABLE snd_file_chunks ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); +CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, connection_id INTEGER NOT NULL, chunk_number INTEGER NOT NULL, chunk_agent_msg_id INTEGER, - chunk_sent INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 0 (sent to agent), 1 (sent to server) - FOREIGN KEY (file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, - PRIMARY KEY (file_id, connection_id, chunk_number) + chunk_sent INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- 0(sent to agent), 1(sent to server) + FOREIGN KEY(file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, + PRIMARY KEY(file_id, connection_id, chunk_number) ) WITHOUT ROWID; -CREATE TABLE rcv_file_chunks ( +CREATE TABLE rcv_file_chunks( file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE, chunk_number INTEGER NOT NULL, chunk_agent_msg_id INTEGER NOT NULL, - chunk_stored INTEGER NOT NULL DEFAULT 0, 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) + chunk_stored INTEGER NOT NULL DEFAULT 0, + 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; -CREATE TABLE connections ( -- all SMP agent connections +CREATE TABLE connections( + -- all SMP agent connections connection_id INTEGER PRIMARY KEY, agent_conn_id BLOB NOT NULL UNIQUE, conn_level INTEGER NOT NULL DEFAULT 0, - via_contact INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL, + via_contact INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL, conn_status TEXT NOT NULL, conn_type TEXT NOT NULL, -- contact, member, rcv_file, snd_file user_contact_link_id INTEGER REFERENCES user_contact_links ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, snd_file_id INTEGER, - rcv_file_id INTEGER REFERENCES rcv_files (file_id) ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), via_contact_uri_hash BLOB, xcontact_id BLOB, - FOREIGN KEY (snd_file_id, connection_id) - REFERENCES snd_files (file_id, connection_id) - ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED + rcv_file_id INTEGER REFERENCES rcv_files(file_id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + via_contact_uri_hash BLOB, + xcontact_id BLOB, + via_user_contact_link INTEGER DEFAULT NULL + REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL, + FOREIGN KEY(snd_file_id, connection_id) + REFERENCES snd_files(file_id, connection_id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED ); -CREATE TABLE user_contact_links ( +CREATE TABLE user_contact_links( user_contact_link_id INTEGER PRIMARY KEY, conn_req_contact BLOB NOT NULL, local_display_name TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), auto_accept INTEGER DEFAULT 0, - UNIQUE (user_id, local_display_name) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + auto_accept INTEGER DEFAULT 0, + auto_reply_msg_content TEXT DEFAULT NULL, + UNIQUE(user_id, local_display_name) ); -CREATE TABLE contact_requests ( +CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links - ON UPDATE CASCADE ON DELETE CASCADE, - agent_invitation_id BLOB NOT NULL, + ON UPDATE CASCADE ON DELETE CASCADE, + agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles - ON DELETE SET NULL -- NULL if it's an incognito profile - DEFERRABLE INITIALLY DEFERRED, - local_display_name TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB, - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON UPDATE CASCADE - ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, contact_profile_id) + ON DELETE SET NULL -- NULL if it's an incognito profile +DEFERRABLE INITIALLY DEFERRED, +local_display_name TEXT NOT NULL, +created_at TEXT NOT NULL DEFAULT(datetime('now')), +user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, +FOREIGN KEY(user_id, local_display_name) +REFERENCES display_names(user_id, local_display_name) +ON UPDATE CASCADE +ON DELETE CASCADE +DEFERRABLE INITIALLY DEFERRED, +UNIQUE(user_id, local_display_name), +UNIQUE(user_id, contact_profile_id) ); -CREATE TABLE messages ( +CREATE TABLE messages( message_id INTEGER PRIMARY KEY, msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent - chat_msg_event TEXT NOT NULL, -- message event tag (the constructor of CMEventTag) + chat_msg_event TEXT NOT NULL, -- message event tag(the constructor of CMEventTag) msg_body BLOB, -- agent message body as received or sent - created_at TEXT NOT NULL DEFAULT (datetime('now')) -, updated_at TEXT CHECK (updated_at NOT NULL), connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE, group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE, shared_msg_id BLOB, shared_msg_id_user INTEGER); -CREATE TABLE msg_deliveries ( + created_at TEXT NOT NULL DEFAULT(datetime('now')) + , + updated_at TEXT CHECK(updated_at NOT NULL), + connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE, + group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE, + shared_msg_id BLOB, + shared_msg_id_user INTEGER +); +CREATE TABLE msg_deliveries( msg_delivery_id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, - agent_msg_id INTEGER, -- internal agent message ID (NULL while pending) + agent_msg_id INTEGER, -- internal agent message ID(NULL while pending) agent_msg_meta TEXT, -- JSON with timestamps etc. sent in MSG, NULL for sent - chat_ts TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- broker_ts for received, created_at for sent - UNIQUE (connection_id, agent_msg_id) + chat_ts TEXT NOT NULL DEFAULT(datetime('now')), + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- broker_ts for received, created_at for sent + UNIQUE(connection_id, agent_msg_id) ); -CREATE TABLE msg_delivery_events ( +CREATE TABLE msg_delivery_events( msg_delivery_event_id INTEGER PRIMARY KEY, msg_delivery_id INTEGER NOT NULL REFERENCES msg_deliveries ON DELETE CASCADE, -- non UNIQUE for multiple events per msg delivery delivery_status TEXT NOT NULL, -- see MsgDeliveryStatus for allowed values - created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL), - UNIQUE (msg_delivery_id, delivery_status) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(msg_delivery_id, delivery_status) ); -CREATE TABLE pending_group_messages ( +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, group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); -CREATE TABLE chat_items ( +CREATE TABLE chat_items( chat_item_id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, -- NULL for sent even if group_id is not chat_msg_id INTEGER, -- sent as part of the message that created the item - created_by_msg_id INTEGER UNIQUE REFERENCES messages (message_id) ON DELETE SET NULL, + created_by_msg_id INTEGER UNIQUE REFERENCES messages(message_id) ON DELETE SET NULL, item_sent INTEGER NOT NULL, -- 0 for received, 1 for sent item_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, item_content TEXT NOT NULL, -- JSON item_text TEXT NOT NULL, -- textual representation - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -, item_status TEXT CHECK (item_status NOT NULL), shared_msg_id BLOB, quoted_shared_msg_id BLOB, quoted_sent_at TEXT, quoted_content TEXT, quoted_sent INTEGER, quoted_member_id BLOB, item_edited INTEGER); -CREATE TABLE chat_item_messages ( + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + item_status TEXT CHECK(item_status NOT NULL), + shared_msg_id BLOB, + quoted_shared_msg_id BLOB, + quoted_sent_at TEXT, + quoted_content TEXT, + quoted_sent INTEGER, + quoted_member_id BLOB, + item_edited INTEGER +); +CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (chat_item_id, message_id) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + UNIQUE(chat_item_id, message_id) ); -CREATE INDEX idx_connections_via_contact_uri_hash ON connections (via_contact_uri_hash); -CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests (xcontact_id); -CREATE INDEX idx_contacts_xcontact_id ON contacts (xcontact_id); -CREATE TABLE smp_servers ( +CREATE INDEX idx_connections_via_contact_uri_hash ON connections( + via_contact_uri_hash +); +CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id); +CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id); +CREATE TABLE smp_servers( smp_server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, port TEXT NOT NULL, key_hash BLOB 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')), - UNIQUE (host, port) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + UNIQUE(host, port) ); -CREATE INDEX idx_messages_shared_msg_id ON messages (shared_msg_id); -CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages (connection_id, shared_msg_id_user, shared_msg_id); -CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages (group_id, shared_msg_id_user, shared_msg_id); -CREATE INDEX idx_chat_items_shared_msg_id ON chat_items (shared_msg_id); +CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); +CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages( + connection_id, + shared_msg_id_user, + shared_msg_id +); +CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages( + group_id, + shared_msg_id_user, + shared_msg_id +); +CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 46da813a49..3c994c47de 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -42,6 +42,7 @@ module Simplex.Chat.Store getUserContactLinkConnections, deleteUserContactLink, getUserContactLink, + getUserContactLinkById, updateUserContactLinkAutoAccept, createOrUpdateContactRequest, getContactRequest, @@ -205,6 +206,7 @@ import Simplex.Chat.Migrations.M20220304_msg_quotes import Simplex.Chat.Migrations.M20220321_chat_item_edited import Simplex.Chat.Migrations.M20220404_files_status_fields import Simplex.Chat.Migrations.M20220514_profiles_user_id +import Simplex.Chat.Migrations.M20220626_auto_reply import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -229,7 +231,8 @@ schemaMigrations = ("20220304_msg_quotes", m20220304_msg_quotes), ("20220321_chat_item_edited", m20220321_chat_item_edited), ("20220404_files_status_fields", m20220404_files_status_fields), - ("20220514_profiles_user_id", m20220514_profiles_user_id) + ("20220514_profiles_user_id", m20220514_profiles_user_id), + ("20220626_auto_reply", m20220626_auto_reply) ] -- | The list of migrations in ascending order by date @@ -335,7 +338,7 @@ getConnReqContactXContactId db userId cReqHash = do -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -366,24 +369,24 @@ createDirectConnection db userId acId pccConnStatus = do pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, createdAt, updatedAt = createdAt} -createContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createContactConnection_ db userId = createConnection_ db userId ConnContact Nothing +createMemberContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection +createMemberContactConnection_ db userId agentConnId viaContact = createConnection_ db userId ConnContact Nothing agentConnId viaContact Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createConnection_ db userId connType entityId acId viaContact connLevel currentTs = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Maybe Int64 -> Int -> UTCTime -> IO Connection +createConnection_ db userId connType entityId acId viaContact viaUserContactLink connLevel currentTs = do DB.execute db [sql| INSERT INTO connections ( - user_id, agent_conn_id, conn_level, via_contact, conn_status, conn_type, + user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, ConnNew, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, connLevel, connStatus = ConnNew, createdAt = currentTs} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, connLevel, connStatus = ConnNew, createdAt = currentTs} where ent ct = if connType == ct then entityId else Nothing @@ -563,7 +566,7 @@ createUserContactLink db userId agentConnId cReq = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing 0 currentTs getUserContactLinkConnections :: DB.Connection -> UserId -> ExceptT StoreError IO [Connection] getUserContactLinkConnections db userId = @@ -573,7 +576,7 @@ getUserContactLinkConnections db userId = DB.queryNamed db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -627,24 +630,37 @@ deleteUserContactLink db userId = do [":user_id" := userId] DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = ''" (Only userId) -getUserContactLink :: DB.Connection -> UserId -> ExceptT StoreError IO (ConnReqContact, Bool) +getUserContactLink :: DB.Connection -> UserId -> ExceptT StoreError IO (ConnReqContact, Bool, Maybe MsgContent) getUserContactLink db userId = ExceptT . firstRow id SEUserContactLinkNotFound $ DB.query db [sql| - SELECT conn_req_contact, auto_accept + SELECT conn_req_contact, auto_accept, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND local_display_name = '' |] (Only userId) -updateUserContactLinkAutoAccept :: DB.Connection -> UserId -> Bool -> ExceptT StoreError IO (ConnReqContact, Bool) -updateUserContactLinkAutoAccept db userId autoAccept = do - (cReqUri, _) <- getUserContactLink db userId +getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> IO (Maybe (ConnReqContact, Bool, Maybe MsgContent)) +getUserContactLinkById db userId userContactLinkId = + maybeFirstRow id $ + DB.query + db + [sql| + SELECT conn_req_contact, auto_accept, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? + AND user_contact_link_id = ? + |] + (userId, userContactLinkId) + +updateUserContactLinkAutoAccept :: DB.Connection -> UserId -> Bool -> Maybe MsgContent -> ExceptT StoreError IO (ConnReqContact, Bool, Maybe MsgContent) +updateUserContactLinkAutoAccept db userId autoAccept msgContent = do + (cReqUri, _, _) <- getUserContactLink db userId liftIO updateUserContactLinkAutoAccept_ - pure (cReqUri, autoAccept) + pure (cReqUri, autoAccept, msgContent) where updateUserContactLinkAutoAccept_ :: IO () updateUserContactLinkAutoAccept_ = @@ -652,11 +668,11 @@ updateUserContactLinkAutoAccept db userId autoAccept = do db [sql| UPDATE user_contact_links - SET auto_accept = ? + SET auto_accept = ?, auto_reply_msg_content = ? WHERE user_id = ? AND local_display_name = '' |] - (autoAccept, userId) + (autoAccept, msgContent, userId) createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image} xContactId_ = @@ -703,7 +719,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -812,8 +828,8 @@ deleteContactRequest db userId contactRequestId = do (userId, userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Maybe XContactId -> IO Contact -createAcceptedContact db userId agentConnId localDisplayName profileId profile xContactId = do +createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Int64 -> Maybe XContactId -> IO Contact +createAcceptedContact db userId agentConnId localDisplayName profileId profile userContactLinkId xContactId = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) currentTs <- getCurrentTime DB.execute @@ -821,7 +837,7 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile x "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" (userId, localDisplayName, profileId, currentTs, currentTs, xContactId) contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing 0 currentTs + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) 0 currentTs pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt = currentTs, updatedAt = currentTs} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] @@ -876,7 +892,7 @@ getPendingConnections db User {userId} = <$> DB.queryNamed db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at FROM connections WHERE user_id = :user_id @@ -893,7 +909,7 @@ getContactConnections db userId Contact {contactId} = DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id @@ -903,14 +919,16 @@ getContactConnections db userId Contact {contactId} = connections [] = throwError $ SEContactNotFound contactId connections rows = pure $ map toConnection rows -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime) +type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, ConnStatus, ConnType) :. EntityIdsRow :. Only UTCTime + +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe ConnStatus, Maybe ConnType) :. EntityIdsRow :. Only (Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection (connId, acId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) = let entityId = entityId_ connType - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, connStatus, connType, entityId, createdAt} + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, connStatus, connType, entityId, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -920,8 +938,8 @@ toConnection (connId, acId, connLevel, viaContact, connStatus, connType, contact entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, Just createdAt) = - Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just connStatus, Just connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) toMaybeConnection _ = Nothing getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact] @@ -1085,7 +1103,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at FROM connections WHERE user_id = ? AND agent_conn_id = ? @@ -1217,7 +1235,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id @@ -1375,7 +1393,7 @@ getGroupMembers db User {userId, userContactId} GroupInfo {groupId} = do SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id @@ -1617,7 +1635,7 @@ createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> Memb createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- liftIO getCurrentTime - Connection {connId = directConnId} <- liftIO $ createContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs + Connection {connId = directConnId} <- liftIO $ createMemberContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile (Just groupId) currentTs liftIO $ do let newMember = @@ -1639,7 +1657,7 @@ createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- getCurrentTime void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs - Connection {connId = directConnId} <- createContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs + Connection {connId = directConnId} <- createMemberContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs where @@ -1669,7 +1687,7 @@ createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createMemberConnection_ db userId groupMemberId = createConnection_ db userId ConnMember (Just groupMemberId) +createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> UTCTime -> IO GroupMember createContactMember_ db user groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy = @@ -1729,7 +1747,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id @@ -1761,7 +1779,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = [sql| SELECT ct.contact_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id @@ -1886,7 +1904,7 @@ getChatRefByFileId db User {userId} fileId = createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection createSndFileConnection_ db userId fileId agentConnId = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing 0 currentTs + createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing 0 currentTs updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -2524,7 +2542,7 @@ getDirectChatPreviews_ db User {userId} = do -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), @@ -2852,7 +2870,7 @@ getContact db userId contactId = -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e3c679372b..f980bb1450 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -669,7 +669,8 @@ data Connection = Connection { connId :: Int64, agentConnId :: AgentConnId, connLevel :: Int, - viaContact :: Maybe Int64, + viaContact :: Maybe Int64, -- group member contact ID, if not direct connection + viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" connType :: ConnType, connStatus :: ConnStatus, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index d2fe0c3d40..835dedce55 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -8,3 +8,6 @@ safeDecodeUtf8 :: ByteString -> Text safeDecodeUtf8 = decodeUtf8With onError where onError _ _ = Just '?' + +uncurry3 :: (a -> b -> c -> d) -> ((a, b, c) -> d) +uncurry3 f ~(a, b, c) = f a b c diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 448e9c6582..670a1cdbd2 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -79,8 +79,8 @@ responseToView testView = \case HSMarkdown -> markdownInfo CRWelcome user -> chatWelcome user CRContactsList cs -> viewContactsList cs - CRUserContactLink cReqUri _ -> connReqContact_ "Your chat address:" cReqUri - CRUserContactLinkUpdated _ autoAccept -> ["auto_accept " <> if autoAccept then "on" else "off"] + CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply + CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] CRGroupCreated g -> viewGroupCreated g CRGroupMembers g -> viewGroupMembers g @@ -361,6 +361,11 @@ connReqContact_ intro cReq = "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] +autoAcceptStatus_ :: Bool -> Maybe MsgContent -> [StyledString] +autoAcceptStatus_ autoAccept autoReply = + ("auto_accept " <> if autoAccept then "on" else "off") : + maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 6a01223d12..43e3b1fbfe 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -82,6 +82,7 @@ chatTests = do it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "reject contact and delete contact link" testRejectContactAndDeleteUserContact it "delete connection requests when contact link deleted" testDeleteConnectionRequests + it "auto-reply message" testAutoReplyMessage describe "SMP servers" $ it "get and set SMP servers" testGetSetSMPServers describe "async connection handshake" $ do @@ -1772,6 +1773,7 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr alice ##> "/sa" cLink' <- getContactLink alice False + alice <## "auto_accept off" cLink' `shouldBe` cLink alice ##> "/da" @@ -1803,6 +1805,28 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ cath ##> ("/c " <> cLink') alice <#? cath +testAutoReplyMessage :: IO () +testAutoReplyMessage = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + alice ##> "/auto_accept on text hello!" + alice <## "auto_accept on" + alice <## "auto reply:" + alice <## "hello!" + + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + alice <## "bob (Bob): accepting contact request..." + concurrentlyN_ + [ do + bob <## "alice (Alice): contact is connected" + bob <# "alice> hello!", + do + alice <## "bob (Bob): contact is connected" + alice <# "@bob hello!" + ] + testGetSetSMPServers :: IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index a876b10afc..bbf9e15588 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -25,6 +25,6 @@ testVerifySchemaDump = void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema savedSchema `seq` pure () - void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " .schema > " <> schema) "" + void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " '.schema --indent' > " <> schema) "" currentSchema <- readFile schema savedSchema `shouldBe` currentSchema