core: support business addresses and chats (#5272)

* core: support business addresses and chats

* types

* connect plan, add link type

* ios: toggle on address UI

* make compile

* todo

* fix migration

* types

* comments

* fix

* remove

* fix schema

* comment

* simplify

* remove diff

* comment

* comment

* diff

* acceptBusinessJoinRequestAsync wip

* comment

* update

* simplify types

* remove business

* wip

* read/write columns

* createBusinessRequestGroup

* remove comments

* read/write business_address column

* validate that business address is not set to be incognito

* replace contact card

* update simplexmq

* refactor

* event when accepting business address request

* sendGroupAutoReply

* delete contact request earlier

* test, fix

* refactor

* refactor2

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny 2024-12-02 14:01:23 +00:00 committed by GitHub
parent c488c4fcd5
commit 5f01dc1a3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 454 additions and 97 deletions

View file

@ -16,6 +16,8 @@ struct UserAddressView: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@State var shareViaProfile = false @State var shareViaProfile = false
@State var autoCreate = false @State var autoCreate = false
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@State private var showMailView = false @State private var showMailView = false
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil @State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
@State private var alert: UserAddressAlert? @State private var alert: UserAddressAlert?
@ -55,7 +57,15 @@ struct UserAddressView: View {
if chatModel.userAddress == nil, autoCreate { if chatModel.userAddress == nil, autoCreate {
createAddress() createAddress()
} }
if let userAddress = chatModel.userAddress {
aas = AutoAcceptState(userAddress: userAddress)
savedAAS = aas
}
} }
.onChange(of: aas.enable) { aasEnabled in
if !aasEnabled { aas = AutoAcceptState() }
}
} }
private func userAddressView() -> some View { private func userAddressView() -> some View {
@ -135,10 +145,23 @@ struct UserAddressView: View {
// if MFMailComposeViewController.canSendMail() { // if MFMailComposeViewController.canSendMail() {
// shareViaEmailButton(userAddress) // shareViaEmailButton(userAddress)
// } // }
settingsRow("hand.wave", color: theme.colors.secondary) {
Toggle("Business address", isOn: $aas.business)
.onChange(of: aas.business) { ba in
if ba {
aas.enable = true
aas.incognito = false
}
}
}
addressSettingsButton(userAddress) addressSettingsButton(userAddress)
} header: { } header: {
Text("For social media") Text("For social media")
.foregroundColor(theme.colors.secondary) .foregroundColor(theme.colors.secondary)
} footer: {
if aas.business {
Text("Add your team members to the conversations").foregroundColor(theme.colors.secondary)
}
} }
Section { Section {
@ -276,11 +299,13 @@ struct UserAddressView: View {
private struct AutoAcceptState: Equatable { private struct AutoAcceptState: Equatable {
var enable = false var enable = false
var incognito = false var incognito = false
var business = false
var welcomeText = "" var welcomeText = ""
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") {
self.enable = enable self.enable = enable
self.incognito = incognito self.incognito = incognito
self.business = business
self.welcomeText = welcomeText self.welcomeText = welcomeText
} }
@ -288,6 +313,7 @@ private struct AutoAcceptState: Equatable {
if let aa = userAddress.autoAccept { if let aa = userAddress.autoAccept {
enable = true enable = true
incognito = aa.acceptIncognito incognito = aa.acceptIncognito
business = aa.businessAddress == true
if let msg = aa.autoReply { if let msg = aa.autoReply {
welcomeText = msg.text welcomeText = msg.text
} else { } else {
@ -296,6 +322,7 @@ private struct AutoAcceptState: Equatable {
} else { } else {
enable = false enable = false
incognito = false incognito = false
business = false
welcomeText = "" welcomeText = ""
} }
} }
@ -305,7 +332,7 @@ private struct AutoAcceptState: Equatable {
var autoReply: MsgContent? = nil var autoReply: MsgContent? = nil
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
if s != "" { autoReply = .text(s) } if s != "" { autoReply = .text(s) }
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply)
} }
return nil return nil
} }
@ -373,7 +400,7 @@ struct UserAddressSettingsView: View {
List { List {
Section { Section {
shareWithContactsButton() shareWithContactsButton()
autoAcceptToggle() autoAcceptToggle().disabled(aas.business)
} }
if aas.enable { if aas.enable {
@ -450,7 +477,9 @@ struct UserAddressSettingsView: View {
private func autoAcceptSection() -> some View { private func autoAcceptSection() -> some View {
Section { Section {
acceptIncognitoToggle() if !aas.business {
acceptIncognitoToggle()
}
welcomeMessageEditor() welcomeMessageEditor()
saveAASButton() saveAASButton()
.disabled(aas == savedAAS) .disabled(aas == savedAAS)

View file

@ -229,6 +229,7 @@
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */; };
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
@ -575,6 +576,7 @@
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -791,6 +793,7 @@
5C971E1F27AEBF7000C8A3CE /* Helpers */ = { 5C971E1F27AEBF7000C8A3CE /* Helpers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */,
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */,
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */, 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */,
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
@ -1445,6 +1448,7 @@
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */,
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */,
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */,
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,

View file

@ -2103,10 +2103,12 @@ public struct UserContactLink: Decodable, Hashable {
} }
public struct AutoAccept: Codable, Hashable { public struct AutoAccept: Codable, Hashable {
public var businessAddress: Bool? // make not nullable
public var acceptIncognito: Bool public var acceptIncognito: Bool
public var autoReply: MsgContent? public var autoReply: MsgContent?
public init(acceptIncognito: Bool, autoReply: MsgContent? = nil) { public init(businessAddress: Bool, acceptIncognito: Bool, autoReply: MsgContent? = nil) {
self.businessAddress = businessAddress
self.acceptIncognito = acceptIncognito self.acceptIncognito = acceptIncognito
self.autoReply = autoReply self.autoReply = autoReply
} }

View file

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 601620bdde612ebdd33da2637d99b15ff32170c9 tag: 38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2
source-repository-package source-repository-package
type: git type: git

View file

@ -0,0 +1,29 @@
# Business address
## Problem
When business uses a communication system for support and other business scenarios, it's important for the customer:
- to be able to talk to multiple people in the business, and know who they are.
- potentially, add friends or relatives to the conversation if this is about a group purchase.
It's important for the business:
- to have bot accept incoming requests.
- to be able to add other people to the coversation, as transfer and as escalation.
This is how all messaging support system works, and how WeChat business accounts work, but no messenger provides it.
## Solution
Make current contact addresses to support business mode. We already have all the elements for that.
- connection requests will be accepted automatically (non-optionally), and auto-reply will be sent (if provided).
- the request sender will be made member, can be made admin later manually.
- the new group with the customer will be created on each request instead of direct conversation.
Group will function differently from a normal group:
- Show business name and avatar to customer, customer name and avatar to business.
- Use different icon for customer and for the business if the avatar is not provided.
- Possibly, a sub-icon on business avatar for customers.
- Members added by business are marked as business, by customer as customer (not MVP).
This functionality allows to develop support bots that automatically reply, potentially answer some questions, and add support agents as required, who can escalate further.

View file

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."601620bdde612ebdd33da2637d99b15ff32170c9" = "0lgiphb9sf5i29d378pah24mhf7m8df75jk6asvw8ns527g4amj1"; "https://github.com/simplex-chat/simplexmq.git"."38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2" = "0nq2a2lklbxpc049zjxa5w8c63l9l9nf08jb7pny42nmah0mlc20";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View file

@ -153,6 +153,7 @@ library
Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
Simplex.Chat.Migrations.M20241027_server_operators Simplex.Chat.Migrations.M20241027_server_operators
Simplex.Chat.Migrations.M20241125_indexes Simplex.Chat.Migrations.M20241125_indexes
Simplex.Chat.Migrations.M20241128_business_chats
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared

View file

@ -2065,6 +2065,8 @@ processChatCommand' vr = \case
SetProfileAddress onOff -> withUser $ \User {userId} -> SetProfileAddress onOff -> withUser $ \User {userId} ->
processChatCommand $ APISetProfileAddress userId onOff processChatCommand $ APISetProfileAddress userId onOff
APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do
forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} ->
when (businessAddress && acceptIncognito) $ throwChatError $ CECommandError "requests to business address cannot be accepted incognito"
contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_)
pure $ CRUserContactLinkUpdated user contactLink pure $ CRUserContactLinkUpdated user contactLink
AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> AddressAutoAccept autoAccept_ -> withUser $ \User {userId} ->
@ -3007,7 +3009,7 @@ processChatCommand' vr = \case
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
pure (groupId, groupMemberId) pure (groupId, groupMemberId)
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv = groupInv =
@ -3016,6 +3018,7 @@ processChatCommand' vr = \case
invitedMember = MemberIdRole memberId memRole, invitedMember = MemberIdRole memberId memRole,
connRequest = cReq, connRequest = cReq,
groupProfile, groupProfile,
businessChat,
groupLinkId = Nothing, groupLinkId = Nothing,
groupSize = Just currentMemCount groupSize = Just currentMemCount
} }
@ -3972,12 +3975,14 @@ acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = Agen
acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember
acceptGroupJoinRequestAsync acceptGroupJoinRequestAsync
user user
gInfo@GroupInfo {groupProfile, membership} gInfo@GroupInfo {groupProfile, membership, businessChat}
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
gLinkMemRole gLinkMemRole
incognitoProfile = do incognitoProfile = do
gVar <- asks random gVar <- asks random
(groupMemberId, memberId) <- withStore $ \db -> createAcceptedMember db gVar user gInfo ucr gLinkMemRole (groupMemberId, memberId) <- withStore $ \db -> do
liftIO $ deleteContactRequestRec db user ucr
createAcceptedMember db gVar user gInfo ucr gLinkMemRole
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let Profile {displayName} = profileToSendOnAccept user incognitoProfile True let Profile {displayName} = profileToSendOnAccept user incognitoProfile True
GroupMember {memberRole = userRole, memberId = userMemberId} = membership GroupMember {memberRole = userRole, memberId = userMemberId} = membership
@ -3988,6 +3993,7 @@ acceptGroupJoinRequestAsync
fromMemberName = displayName, fromMemberName = displayName,
invitedMember = MemberIdRole memberId gLinkMemRole, invitedMember = MemberIdRole memberId gLinkMemRole,
groupProfile, groupProfile,
businessChat,
groupSize = Just currentMemCount groupSize = Just currentMemCount
} }
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
@ -3998,6 +4004,43 @@ acceptGroupJoinRequestAsync
liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode
getGroupMemberById db vr user groupMemberId getGroupMemberById db vr user groupMemberId
acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo
acceptBusinessJoinRequestAsync
user
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} = do
vr <- chatVersionRange
gVar <- asks random
let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
(gInfo, clientMember) <- withStore $ \db -> do
liftIO $ deleteContactRequestRec db user ucr
createBusinessRequestGroup db vr gVar user ucr groupPreferences
let GroupInfo {membership} = gInfo
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
GroupMember {groupMemberId, memberId} = clientMember
msg =
XGrpLinkInv $
GroupLinkInvitation
{ fromMember = MemberIdRole userMemberId userRole,
fromMemberName = displayName,
invitedMember = MemberIdRole memberId GRMember,
groupProfile = businessGroupProfile userProfile groupPreferences,
-- This refers to the "title member" that defines the group name and profile.
-- This coincides with fromMember to be current user when accepting the connecting user,
-- but it will be different when inviting somebody else.
businessChat = Just $ BusinessChatInfo userMemberId BCBusiness,
groupSize = Just 1
}
subMode <- chatReadVar subscriptionMode
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
withStore' $ \db -> createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode
pure gInfo
where
businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
businessGroupProfile Profile {displayName, fullName, image} groupPreferences =
GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences}
profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile
profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing
where where
@ -4683,15 +4726,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
CONF confId pqSupport _ connInfo -> do CONF confId pqSupport _ connInfo -> do
conn' <- processCONFpqSupport conn pqSupport conn' <- processCONFpqSupport conn pqSupport
-- [incognito] send saved profile -- [incognito] send saved profile
(conn'', inGroup) <- saveConnInfo conn' connInfo
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing False let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing inGroup
conn'' <- saveConnInfo conn' connInfo
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend
INFO pqSupport connInfo -> do INFO pqSupport connInfo -> do
processINFOpqSupport conn pqSupport processINFOpqSupport conn pqSupport
_conn' <- saveConnInfo conn connInfo void $ saveConnInfo conn connInfo
pure ()
MSG meta _msgFlags _msgBody -> MSG meta _msgFlags _msgBody ->
-- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet, -- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet,
-- chat item is also not created here -- chat item is also not created here
@ -4806,6 +4848,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False
allowAgentConnectionAsync user conn'' confId $ XInfo p allowAgentConnectionAsync user conn'' confId $ XInfo p
void $ withStore' $ \db -> resetMemberContactFields db ct' void $ withStore' $ \db -> resetMemberContactFields db ct'
XGrpLinkInv glInv -> do
-- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group
(gInfo, host) <- withStore $ \db -> do
liftIO $ deleteContactCardKeepConn db connId ct
createGroupInvitedViaLink db vr user conn'' glInv
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing True
allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend
toView $ CRBusinessLinkConnecting user gInfo host ct
_ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info"
INFO pqSupport connInfo -> do INFO pqSupport connInfo -> do
processINFOpqSupport conn pqSupport processINFOpqSupport conn pqSupport
@ -4936,7 +4988,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> pure () _ -> pure ()
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
INV (ACR _ cReq) -> INV (ACR _ cReq) ->
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
case cReq of case cReq of
@ -4977,6 +5029,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
invitedMember = MemberIdRole memberId memRole, invitedMember = MemberIdRole memberId memRole,
connRequest = cReq, connRequest = cReq,
groupProfile, groupProfile,
businessChat = Nothing,
groupLinkId = groupLinkId, groupLinkId = groupLinkId,
groupSize = Just currentMemCount groupSize = Just currentMemCount
} }
@ -5049,6 +5102,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m
sendIntroductions members sendIntroductions members
when (groupFeatureAllowed SGFHistory gInfo) sendHistory when (groupFeatureAllowed SGFHistory gInfo) sendHistory
when (connChatVersion < batchSend2Version) $ sendGroupAutoReply members
where where
sendXGrpLinkMem = do sendXGrpLinkMem = do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
@ -5311,9 +5365,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
OK -> OK ->
-- [async agent commands] continuation on receiving OK -- [async agent commands] continuation on receiving OK
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
JOINED _ -> JOINED sqSecured ->
-- [async agent commands] continuation on receiving JOINED -- [async agent commands] continuation on receiving JOINED
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData ->
when sqSecured $ do
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
when (connChatVersion >= batchSend2Version) $ sendGroupAutoReply members
QCONT -> do QCONT -> do
continued <- continueSending connEntity conn continued <- continueSending connEntity conn
when continued $ sendPendingGroupMessages user m conn when continued $ sendPendingGroupMessages user m conn
@ -5341,6 +5398,23 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do
itemIds <- getChatItemIdsByAgentMsgId db connId msgId itemIds <- getChatItemIdsByAgentMsgId db connId msgId
forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus
sendGroupAutoReply members = autoReplyMC >>= mapM_ send
where
autoReplyMC = do
let GroupInfo {businessChat} = gInfo
GroupMember {memberId = joiningMemberId} = m
case businessChat of
Just BusinessChatInfo {memberId, chatType = BCCustomer}
| joiningMemberId == memberId -> useReply <$> withStore (`getUserAddress` user)
where
useReply UserContactLink {autoAccept} = case autoAccept of
Just AutoAccept {businessAddress, autoReply} | businessAddress -> autoReply
_ -> Nothing
_ -> pure Nothing
send mc = do
msg <- sendGroupMessage' user gInfo members (XMsgNew $ MCSimple (extMsgContent mc Nothing))
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc)
toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci]
agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32)
agentMsgDecryptError = \case agentMsgDecryptError = \case
@ -5525,26 +5599,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
CORRequest cReq -> do CORRequest cReq -> do
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl let (UserContactLink {connReqContact, autoAccept}, groupId_, gLinkMemRole) = ucl
isSimplexTeam = sameConnReqContact connReqContact adminContactReq
v = maxVersion chatVRange
case autoAccept of case autoAccept of
Just AutoAccept {acceptIncognito} -> case groupId_ of Just AutoAccept {acceptIncognito, businessAddress}
Nothing -> do | businessAddress ->
-- [incognito] generate profile to send, create connection with incognito profile if v < groupFastLinkJoinVersion || (isSimplexTeam && v < businessChatsVersion)
incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing then do
ct <- acceptContactRequestAsync user cReq incognitoProfile True reqPQSup ct <- acceptContactRequestAsync user cReq Nothing True reqPQSup
toView $ CRAcceptingContactRequest user ct toView $ CRAcceptingContactRequest user ct
Just groupId -> do else do
gInfo <- withStore $ \db -> getGroupInfo db vr user groupId gInfo <- acceptBusinessJoinRequestAsync user cReq
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo toView $ CRAcceptingBusinessRequest user gInfo
if maxVersion chatVRange >= groupFastLinkJoinVersion | otherwise -> case groupId_ of
then do Nothing -> do
mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode -- [incognito] generate profile to send, create connection with incognito profile
createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
toView $ CRAcceptingGroupJoinRequestMember user gInfo mem ct <- acceptContactRequestAsync user cReq incognitoProfile True reqPQSup
else do toView $ CRAcceptingContactRequest user ct
-- TODO v5.7 remove old API (or v6.0?) Just groupId -> do
ct <- acceptContactRequestAsync user cReq profileMode False PQSupportOff gInfo <- withStore $ \db -> getGroupInfo db vr user groupId
toView $ CRAcceptingGroupJoinRequest user gInfo ct let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
if v >= groupFastLinkJoinVersion
then do
mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode
createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CRAcceptingGroupJoinRequestMember user gInfo mem
else do
-- TODO v5.7 remove old API (or v6.0?)
ct <- acceptContactRequestAsync user cReq profileMode False PQSupportOff
toView $ CRAcceptingGroupJoinRequest user gInfo ct
_ -> toView $ CRReceivedContactRequest user cReq _ -> toView $ CRReceivedContactRequest user cReq
memberCanSend :: GroupMember -> CM () -> CM () memberCanSend :: GroupMember -> CM () -> CM ()
@ -6353,9 +6438,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs)
xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM ()
xGrpLinkMem gInfo@GroupInfo {membership} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do
xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId
if viaGroupLink && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived
then do then do
m' <- processMemberProfileUpdate gInfo m p' False Nothing m' <- processMemberProfileUpdate gInfo m p' False Nothing
withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True
@ -6652,7 +6737,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
toView $ CRContactAndMemberAssociated user c2 g m1 c2' toView $ CRContactAndMemberAssociated user c2 g m1 c2'
pure c2' pure c2'
saveConnInfo :: Connection -> ConnInfo -> CM Connection saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Bool)
saveConnInfo activeConn connInfo = do saveConnInfo activeConn connInfo = do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
conn' <- updatePeerChatVRange activeConn chatVRange conn' <- updatePeerChatVRange activeConn chatVRange
@ -6661,13 +6746,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let contactUsed = connDirect activeConn let contactUsed = connDirect activeConn
ct <- withStore $ \db -> createDirectContact db user conn' p contactUsed ct <- withStore $ \db -> createDirectContact db user conn' p contactUsed
toView $ CRContactConnecting user ct toView $ CRContactConnecting user ct
pure conn' pure (conn', False)
XGrpLinkInv glInv -> do XGrpLinkInv glInv -> do
(gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv
toView $ CRGroupLinkConnecting user gInfo host toView $ CRGroupLinkConnecting user gInfo host
pure conn' pure (conn', True)
-- TODO show/log error, other events in SMP confirmation -- TODO show/log error, other events in SMP confirmation
_ -> pure conn' _ -> pure (conn', False)
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> CM () xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> CM ()
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do
@ -8683,11 +8768,11 @@ chatCommandP =
dbKeyP = nonEmptyKey <$?> strP dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
autoAcceptP = autoAcceptP = ifM onOffP (Just <$> (businessAA <|> addressAA)) (pure Nothing)
ifM where
onOffP addressAA = AutoAccept False <$> (" incognito=" *> onOffP <|> pure False) <*> autoReply
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) businessAA = AutoAccept True <$> (" business" *> pure False) <*> autoReply
(pure Nothing) autoReply = optional (A.space *> msgContentP)
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char char_ = optional . A.char

View file

@ -56,7 +56,7 @@ initializeBotAddress' logAddress cc = do
where where
showBotAddress uri = do showBotAddress uri = do
when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri)
void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing} void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing}
sendMessage :: ChatController -> Contact -> Text -> IO () sendMessage :: ChatController -> Contact -> Text -> IO ()
sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText

View file

@ -639,6 +639,7 @@ data ChatResponse
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest}
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
| CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
| CRBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact}
| CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]}
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
@ -665,6 +666,7 @@ data ChatResponse
| CRUserContactLinkDeleted {user :: User} | CRUserContactLinkDeleted {user :: User}
| CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest}
| CRAcceptingContactRequest {user :: User, contact :: Contact} | CRAcceptingContactRequest {user :: User, contact :: Contact}
| CRAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo}
| CRContactAlreadyExists {user :: User, contact :: Contact} | CRContactAlreadyExists {user :: User, contact :: Contact}
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}

View file

@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20241128_business_chats where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20241128_business_chats :: Query
m20241128_business_chats =
[sql|
ALTER TABLE user_contact_links ADD business_address INTEGER DEFAULT 0;
ALTER TABLE groups ADD COLUMN business_member_id BLOB NULL;
ALTER TABLE groups ADD COLUMN business_chat TEXT NULL;
|]
down_m20241128_business_chats :: Query
down_m20241128_business_chats =
[sql|
ALTER TABLE user_contact_links DROP COLUMN business_address;
ALTER TABLE groups DROP COLUMN business_member_id;
ALTER TABLE groups DROP COLUMN business_chat;
|]

View file

@ -127,7 +127,9 @@ CREATE TABLE groups(
via_group_link_uri_hash BLOB, via_group_link_uri_hash BLOB,
user_member_profile_sent_at TEXT, user_member_profile_sent_at TEXT,
custom_data BLOB, custom_data BLOB,
ui_themes TEXT, -- received ui_themes TEXT,
business_member_id BLOB NULL,
business_chat TEXT NULL, -- received
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
@ -309,6 +311,7 @@ CREATE TABLE user_contact_links(
auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL), auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL),
group_link_id BLOB, group_link_id BLOB,
group_link_member_role TEXT NULL, group_link_member_role TEXT NULL,
business_address INTEGER DEFAULT 0,
UNIQUE(user_id, local_display_name) UNIQUE(user_id, local_display_name)
); );
CREATE TABLE contact_requests( CREATE TABLE contact_requests(

View file

@ -66,12 +66,13 @@ import Simplex.Messaging.Version hiding (version)
-- 7 - update member profiles (1/15/2024) -- 7 - update member profiles (1/15/2024)
-- 8 - compress messages and PQ e2e encryption (2024-03-08) -- 8 - compress messages and PQ e2e encryption (2024-03-08)
-- 9 - batch sending in direct connections (2024-07-24) -- 9 - batch sending in direct connections (2024-07-24)
-- 10 - business chats (2024-11-29)
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
-- This indirection is needed for backward/forward compatibility testing. -- This indirection is needed for backward/forward compatibility testing.
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
currentChatVersion :: VersionChat currentChatVersion :: VersionChat
currentChatVersion = VersionChat 9 currentChatVersion = VersionChat 10
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
supportedChatVRange :: VersionRangeChat supportedChatVRange :: VersionRangeChat
@ -110,6 +111,10 @@ pqEncryptionCompressionVersion = VersionChat 8
batchSend2Version :: VersionChat batchSend2Version :: VersionChat
batchSend2Version = VersionChat 9 batchSend2Version = VersionChat 9
-- supports differentiating business chats when joining contact addresses
businessChatsVersion :: VersionChat
businessChatsVersion = VersionChat 10
agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion :: VersionSMPA -> VersionChat
agentToChatVersion v agentToChatVersion v
| v < pqdrSMPAgentVersion = initialChatVersion | v < pqdrSMPAgentVersion = initialChatVersion

View file

@ -123,7 +123,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,

View file

@ -30,6 +30,7 @@ module Simplex.Chat.Store.Groups
getGroupAndMember, getGroupAndMember,
createNewGroup, createNewGroup,
createGroupInvitation, createGroupInvitation,
deleteContactCardKeepConn,
createGroupInvitedViaLink, createGroupInvitedViaLink,
setViaGroupLinkHash, setViaGroupLinkHash,
setGroupInvitationChatItemId, setGroupInvitationChatItemId,
@ -62,6 +63,7 @@ module Simplex.Chat.Store.Groups
createNewContactMemberAsync, createNewContactMemberAsync,
createAcceptedMember, createAcceptedMember,
createAcceptedMemberConnection, createAcceptedMemberConnection,
createBusinessRequestGroup,
getContactViaMember, getContactViaMember,
setNewContactMemberConnRequest, setNewContactMemberConnRequest,
getMemberInvitation, getMemberInvitation,
@ -153,19 +155,20 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import UnliftIO.STM import UnliftIO.STM
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow
type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData) :. userMemberRow) = toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, businessMemberId, businessChatType, uiThemes, customData) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} businessChat = BusinessChatInfo <$> businessMemberId <*> businessChatType
in GroupInfo {groupId, localDisplayName, groupProfile, businessChat, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
@ -276,7 +279,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@ -342,6 +345,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
{ groupId, { groupId,
localDisplayName = ldn, localDisplayName = ldn,
groupProfile, groupProfile,
businessChat = Nothing,
fullGroupPreferences, fullGroupPreferences,
membership, membership,
hostConnCustomUserProfileId = Nothing, hostConnCustomUserProfileId = Nothing,
@ -357,7 +361,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
-- | creates a new group record for the group the current user was invited to, or returns an existing one -- | creates a new group record for the group the current user was invited to, or returns an existing one
createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName
createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, businessChat} incognitoProfileId = do
liftIO getInvitationGroupId_ >>= \case liftIO getInvitationGroupId_ >>= \case
Nothing -> createGroupInvitation_ Nothing -> createGroupInvitation_
Just gId -> do Just gId -> do
@ -395,10 +399,10 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
[sql| [sql|
INSERT INTO groups INSERT INTO groups
(group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs,
created_at, updated_at, chat_ts, user_member_profile_sent_at) created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat)
VALUES (?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|] |]
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) ((profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat)
insertedRowId db insertedRowId db
let hostVRange = adjustedMemberVRange vr peerChatVRange let hostVRange = adjustedMemberVRange vr peerChatVRange
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange
@ -409,6 +413,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
{ groupId, { groupId,
localDisplayName, localDisplayName,
groupProfile, groupProfile,
businessChat = Nothing,
fullGroupPreferences, fullGroupPreferences,
membership, membership,
hostConnCustomUserProfileId = customUserProfileId, hostConnCustomUserProfileId = customUserProfileId,
@ -423,6 +428,11 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
groupMemberId groupMemberId
) )
businessChatTuple :: Maybe BusinessChatInfo -> (Maybe MemberId, Maybe BusinessChatType)
businessChatTuple = \case
Just BusinessChatInfo {memberId, chatType} -> (Just memberId, Just chatType)
Nothing -> (Nothing, Nothing)
adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat
adjustedMemberVRange chatVR vr@(VersionRange minV maxV) = adjustedMemberVRange chatVR vr@(VersionRange minV maxV) =
let maxV' = min maxV (maxVersion chatVR) let maxV' = min maxV (maxVersion chatVR)
@ -497,13 +507,19 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
) )
pure $ Right incognitoLdn pure $ Right incognitoLdn
deleteContactCardKeepConn :: DB.Connection -> Int64 -> Contact -> IO ()
deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile {profileId}} = do
DB.execute db "UPDATE connections SET contact_id = NULL WHERE connection_id = ?" (Only connId)
DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId)
DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId)
createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
createGroupInvitedViaLink createGroupInvitedViaLink
db db
vr vr
user@User {userId, userContactId} user@User {userId, userContactId}
Connection {connId, customUserProfileId} Connection {connId, customUserProfileId}
GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile} = do GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, businessChat} = do
currentTs <- liftIO getCurrentTime currentTs <- liftIO getCurrentTime
groupId <- insertGroup_ currentTs groupId <- insertGroup_ currentTs
hostMemberId <- insertHost_ currentTs groupId hostMemberId <- insertHost_ currentTs groupId
@ -527,10 +543,10 @@ createGroupInvitedViaLink
[sql| [sql|
INSERT INTO groups INSERT INTO groups
(group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs,
created_at, updated_at, chat_ts, user_member_profile_sent_at) created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat)
VALUES (?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|] |]
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) ((profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat)
insertedRowId db insertedRowId db
insertHost_ currentTs groupId = do insertHost_ currentTs groupId = do
let fromMemberProfile = profileFromName fromMemberName let fromMemberProfile = profileFromName fromMemberName
@ -637,7 +653,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
SELECT SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g FROM groups g
@ -879,9 +895,7 @@ createAcceptedMember
User {userId, userContactId} User {userId, userContactId}
GroupInfo {groupId, membership} GroupInfo {groupId, membership}
UserContactRequest {cReqChatVRange, localDisplayName, profileId} UserContactRequest {cReqChatVRange, localDisplayName, profileId}
memberRole = do memberRole =
liftIO $
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createWithRandomId gVar $ \memId -> do createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt insertMember_ (MemberId memId) createdAt
@ -917,6 +931,46 @@ createAcceptedMemberConnection
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
setCommandConnId db user cmdId connId setCommandConnId db user cmdId connId
createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> UserContactRequest -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember)
createBusinessRequestGroup
db
vr
gVar
user@User {userId}
ucr@UserContactRequest {profile}
groupPreferences = do
currentTs <- liftIO getCurrentTime
groupInfo <- insertGroup_ currentTs
(groupMemberId, memberId) <- createAcceptedMember db gVar user groupInfo ucr GRMember
liftIO $ setBusinessMemberId groupInfo memberId
acceptedMember <- getGroupMemberById db vr user groupMemberId
pure (groupInfo, acceptedMember)
where
insertGroup_ currentTs = ExceptT $ do
let Profile {displayName, fullName, image} = profile
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
[sql|
INSERT INTO groups
(group_profile_id, local_display_name, user_id, enable_ntfs,
created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat)
VALUES (?,?,?,?,?,?,?,?,?)
|]
(profileId, localDisplayName, userId, True, currentTs, currentTs, currentTs, currentTs, BCCustomer)
insertedRowId db
memberId <- liftIO $ encodedRandomBytes gVar 12
void $ createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr
getGroupInfo db vr user groupId
setBusinessMemberId GroupInfo {groupId} businessMemberId = do
DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (businessMemberId, groupId)
getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact
getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do
contactId <- contactId <-
@ -1315,7 +1369,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@ -1411,7 +1465,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data,
-- GroupMember - membership -- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,

View file

@ -117,6 +117,7 @@ import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
import Simplex.Chat.Migrations.M20241027_server_operators import Simplex.Chat.Migrations.M20241027_server_operators
import Simplex.Chat.Migrations.M20241125_indexes import Simplex.Chat.Migrations.M20241125_indexes
import Simplex.Chat.Migrations.M20241128_business_chats
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -233,7 +234,8 @@ schemaMigrations =
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id),
("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id), ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id),
("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators), ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators),
("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes) ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes),
("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View file

@ -445,7 +445,8 @@ data UserContactLink = UserContactLink
deriving (Show) deriving (Show)
data AutoAccept = AutoAccept data AutoAccept = AutoAccept
{ acceptIncognito :: IncognitoEnabled, { businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type
acceptIncognito :: IncognitoEnabled,
autoReply :: Maybe MsgContent autoReply :: Maybe MsgContent
} }
deriving (Show) deriving (Show)
@ -454,10 +455,10 @@ $(J.deriveJSON defaultJSON ''AutoAccept)
$(J.deriveJSON defaultJSON ''UserContactLink) $(J.deriveJSON defaultJSON ''UserContactLink)
toUserContactLink :: (ConnReqContact, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink toUserContactLink :: (ConnReqContact, Bool, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink
toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) = toUserContactLink (connReq, autoAccept, businessAddress, acceptIncognito, autoReply) =
UserContactLink connReq $ UserContactLink connReq $
if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing
getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink
getUserAddress db User {userId} = getUserAddress db User {userId} =
@ -465,7 +466,7 @@ getUserAddress db User {userId} =
DB.query DB.query
db db
[sql| [sql|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
|] |]
@ -477,7 +478,7 @@ getUserContactLinkById db userId userContactLinkId =
DB.query DB.query
db db
[sql| [sql|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? WHERE user_id = ?
AND user_contact_link_id = ? AND user_contact_link_id = ?
@ -490,7 +491,7 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
DB.query DB.query
db db
[sql| [sql|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? AND conn_req_contact IN (?,?) WHERE user_id = ? AND conn_req_contact IN (?,?)
|] |]
@ -522,13 +523,13 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do
db db
[sql| [sql|
UPDATE user_contact_links UPDATE user_contact_links
SET auto_accept = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? SET auto_accept = ?, business_address = ?, auto_accept_incognito = ?, auto_reply_msg_content = ?
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
|] |]
(ucl :. Only userId) (ucl :. Only userId)
ucl = case autoAccept of ucl = case autoAccept of
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> (True, businessAddress, acceptIncognito, autoReply)
_ -> (False, False, Nothing) _ -> (False, False, False, Nothing)
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p]
getProtocolServers db p User {userId} = getProtocolServers db p User {userId} =
@ -589,7 +590,7 @@ getServerOperators db = do
let conditionsAction = usageConditionsAction ops currentConditions now let conditionsAction = usageConditionsAction ops currentConditions now
pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction} pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction}
getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
getUserServers db user = getUserServers db user =
(,,) (,,)
<$> (map Just . serverOperators <$> getServerOperators db) <$> (map Just . serverOperators <$> getServerOperators db)
@ -620,7 +621,8 @@ getUpdateServerOperators db presetOps newUser = do
mapM_ insertConditions condsToAdd mapM_ insertConditions condsToAdd
latestAcceptedConds_ <- getLatestAcceptedConditions db latestAcceptedConds_ <- getLatestAcceptedConditions db
ops <- updatedServerOperators presetOps <$> getServerOperators_ db ops <- updatedServerOperators presetOps <$> getServerOperators_ db
forM ops $ traverse $ mapM $ \(ASO _ op) -> -- traverse for tuple, mapM for Maybe forM ops $ traverse $ mapM $ \(ASO _ op) ->
-- traverse for tuple, mapM for Maybe
case operatorId op of case operatorId op of
DBNewEntity -> do DBNewEntity -> do
op' <- insertOperator op op' <- insertOperator op
@ -765,8 +767,9 @@ acceptConditions db condId opIds acceptedAt = do
liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts
where where
getServerOperator_ opId = getServerOperator_ opId =
ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $ ExceptT $
DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId) firstRow toServerOperator (SEOperatorNotFound opId) $
DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId)
acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO ()
acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt =

View file

@ -52,7 +52,7 @@ import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..),
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal import Simplex.Messaging.Version.Internal
@ -371,6 +371,7 @@ data GroupInfo = GroupInfo
{ groupId :: GroupId, { groupId :: GroupId,
localDisplayName :: GroupName, localDisplayName :: GroupName,
groupProfile :: GroupProfile, groupProfile :: GroupProfile,
businessChat :: Maybe BusinessChatInfo,
fullGroupPreferences :: FullGroupPreferences, fullGroupPreferences :: FullGroupPreferences,
membership :: GroupMember, membership :: GroupMember,
hostConnCustomUserProfileId :: Maybe ProfileId, hostConnCustomUserProfileId :: Maybe ProfileId,
@ -384,6 +385,24 @@ data GroupInfo = GroupInfo
} }
deriving (Eq, Show) deriving (Eq, Show)
data BusinessChatType
= BCBusiness -- used on the customer side
| BCCustomer -- used on the business side
deriving (Eq, Show)
instance TextEncoding BusinessChatType where
textEncode = \case
BCBusiness -> "business"
BCCustomer -> "customer"
textDecode = \case
"business" -> Just BCBusiness
"customer" -> Just BCCustomer
_ -> Nothing
instance FromField BusinessChatType where fromField = fromTextField_ textDecode
instance ToField BusinessChatType where toField = toField . textEncode
groupName' :: GroupInfo -> GroupName groupName' :: GroupInfo -> GroupName
groupName' GroupInfo {localDisplayName = g} = g groupName' GroupInfo {localDisplayName = g} = g
@ -598,6 +617,7 @@ data GroupInvitation = GroupInvitation
invitedMember :: MemberIdRole, invitedMember :: MemberIdRole,
connRequest :: ConnReqInvitation, connRequest :: ConnReqInvitation,
groupProfile :: GroupProfile, groupProfile :: GroupProfile,
businessChat :: Maybe BusinessChatInfo,
groupLinkId :: Maybe GroupLinkId, groupLinkId :: Maybe GroupLinkId,
groupSize :: Maybe Int groupSize :: Maybe Int
} }
@ -608,6 +628,7 @@ data GroupLinkInvitation = GroupLinkInvitation
fromMemberName :: ContactName, fromMemberName :: ContactName,
invitedMember :: MemberIdRole, invitedMember :: MemberIdRole,
groupProfile :: GroupProfile, groupProfile :: GroupProfile,
businessChat :: Maybe BusinessChatInfo,
groupSize :: Maybe Int groupSize :: Maybe Int
} }
deriving (Eq, Show) deriving (Eq, Show)
@ -632,6 +653,12 @@ data MemberInfo = MemberInfo
} }
deriving (Eq, Show) deriving (Eq, Show)
data BusinessChatInfo = BusinessChatInfo
{ memberId :: MemberId,
chatType :: BusinessChatType
}
deriving (Eq, Show)
memberInfo :: GroupMember -> MemberInfo memberInfo :: GroupMember -> MemberInfo
memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} = memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
MemberInfo MemberInfo
@ -1696,6 +1723,10 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MF") ''MsgFilter)
$(JQ.deriveJSON defaultJSON ''ChatSettings) $(JQ.deriveJSON defaultJSON ''ChatSettings)
$(JQ.deriveJSON (enumJSON $ dropPrefix "BC") ''BusinessChatType)
$(JQ.deriveJSON defaultJSON ''BusinessChatInfo)
$(JQ.deriveJSON defaultJSON ''GroupInfo) $(JQ.deriveJSON defaultJSON ''GroupInfo)
$(JQ.deriveJSON defaultJSON ''Group) $(JQ.deriveJSON defaultJSON ''Group)
@ -1706,18 +1737,18 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
instance ToField MsgFilter where toField = toField . msgFilterInt instance ToField MsgFilter where toField = toField . msgFilterInt
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "CRData") ''CReqClientData) $(JQ.deriveJSON defaultJSON ''CReqClientData)
$(JQ.deriveJSON defaultJSON ''MemberIdRole) $(JQ.deriveJSON defaultJSON ''MemberIdRole)
$(JQ.deriveJSON defaultJSON ''MemberInfo)
$(JQ.deriveJSON defaultJSON ''GroupInvitation) $(JQ.deriveJSON defaultJSON ''GroupInvitation)
$(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation)
$(JQ.deriveJSON defaultJSON ''IntroInvitation) $(JQ.deriveJSON defaultJSON ''IntroInvitation)
$(JQ.deriveJSON defaultJSON ''MemberInfo)
$(JQ.deriveJSON defaultJSON ''MemberRestrictions) $(JQ.deriveJSON defaultJSON ''MemberRestrictions)
$(JQ.deriveJSON defaultJSON ''GroupMemberRef) $(JQ.deriveJSON defaultJSON ''GroupMemberRef)

View file

@ -390,6 +390,33 @@ defaultGroupPrefs =
emptyGroupPrefs :: GroupPreferences emptyGroupPrefs :: GroupPreferences
emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
businessGroupPrefs :: Preferences -> GroupPreferences
businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice} =
defaultBusinessGroupPrefs
{ timedMessages = Just TimedMessagesGroupPreference {enable = maybe FEOff enableFeature timedMessages, ttl = maybe Nothing prefParam timedMessages},
fullDelete = Just FullDeleteGroupPreference {enable = maybe FEOff enableFeature fullDelete},
reactions = Just ReactionsGroupPreference {enable = maybe FEOn enableFeature reactions},
voice = Just VoiceGroupPreference {enable = maybe FEOff enableFeature voice, role = Nothing}
}
where
enableFeature :: FeatureI f => FeaturePreference f -> GroupFeatureEnabled
enableFeature p = case getField @"allow" p of
FANo -> FEOff
_ -> FEOn
defaultBusinessGroupPrefs :: GroupPreferences
defaultBusinessGroupPrefs =
GroupPreferences
{ timedMessages = Just $ TimedMessagesGroupPreference FEOff Nothing,
directMessages = Just $ DirectMessagesGroupPreference FEOff Nothing,
fullDelete = Just $ FullDeleteGroupPreference FEOff,
reactions = Just $ ReactionsGroupPreference FEOn,
voice = Just $ VoiceGroupPreference FEOff Nothing,
files = Just $ FilesGroupPreference FEOn Nothing,
simplexLinks = Just $ SimplexLinksGroupPreference FEOn Nothing,
history = Just $ HistoryGroupPreference FEOn
}
data TimedMessagesPreference = TimedMessagesPreference data TimedMessagesPreference = TimedMessagesPreference
{ allow :: FeatureAllowed, { allow :: FeatureAllowed,
ttl :: Maybe Int ttl :: Maybe Int

View file

@ -204,12 +204,14 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c
CRAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g
CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"]
CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"]
CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq
CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted
CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."]
CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."]
CRBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."]
CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g
CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um]
@ -979,9 +981,14 @@ simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simpl
autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString]
autoAcceptStatus_ = \case autoAcceptStatus_ = \case
Just AutoAccept {acceptIncognito, autoReply} -> Just AutoAccept {businessAddress, acceptIncognito, autoReply} ->
("auto_accept on" <> if acceptIncognito then ", incognito" else "") ("auto_accept on" <> aaInfo)
: maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply : maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
where
aaInfo
| businessAddress = ", business"
| acceptIncognito = ", incognito"
| otherwise = ""
_ -> ["auto_accept off"] _ -> ["auto_accept off"]
groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString] groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString]
@ -1017,6 +1024,9 @@ viewAcceptingContactRequest ct
| contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"] | contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"]
| otherwise = [ttyFullContact ct <> ": accepting contact request..."] | otherwise = [ttyFullContact ct <> ": accepting contact request..."]
viewAcceptingBusinessRequest :: GroupInfo -> [StyledString]
viewAcceptingBusinessRequest g = [ttyFullGroup g <> ": accepting business address request..."]
viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest :: ContactName -> Profile -> [StyledString]
viewReceivedContactRequest c Profile {fullName} = viewReceivedContactRequest c Profile {fullName} =
[ ttyFullName c fullName <> " wants to connect to you!", [ ttyFullName c fullName <> " wants to connect to you!",

View file

@ -47,6 +47,8 @@ chatProfileTests = do
it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "delete connection requests when contact link deleted" testDeleteConnectionRequests
it "auto-reply message" testAutoReplyMessage it "auto-reply message" testAutoReplyMessage
it "auto-reply message in incognito" testAutoReplyMessageInIncognito it "auto-reply message in incognito" testAutoReplyMessageInIncognito
describe "business address" $ do
it "create and connect via business address" testBusinessAddress
describe "contact address connection plan" $ do describe "contact address connection plan" $ do
it "contact address ok to connect; known contact" testPlanAddressOkKnown it "contact address ok to connect; known contact" testPlanAddressOkKnown
it "own contact address" testPlanAddressOwn it "own contact address" testPlanAddressOwn
@ -677,6 +679,49 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
alice <## "use /i bob to print out this incognito profile again" alice <## "use /i bob to print out this incognito profile again"
] ]
testBusinessAddress :: HasCallStack => FilePath -> IO ()
testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $
\biz alice bob -> do
biz ##> "/ad"
cLink <- getContactLink biz True
biz ##> "/auto_accept on business"
biz <## "auto_accept on, business"
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
biz <## "#bob_1 (Bob): accepting business address request..."
biz <## "#bob_1: bob joined the group"
bob <## "#biz: joining the group..."
bob <## "#biz: you joined the group"
biz #> "#bob_1 hi"
bob <# "#biz biz_1> hi"
bob #> "#biz hello"
biz <# "#bob_1 bob> hello"
connectUsers biz alice
biz <##> alice
biz ##> "/a #bob_1 alice"
biz <## "invitation to join the group #bob_1 sent to alice"
alice <## "#bob (Bob): biz invites you to join the group as member"
alice <## "use /j bob to accept"
alice ##> "/j bob"
concurrentlyN_
[ do
alice <## "#bob: you joined the group"
alice <### [WithTime "#bob biz> hi [>>]", WithTime "#bob bob_1> hello [>>]"]
alice <## "#bob: member bob_1 (Bob) is connected",
biz <## "#bob_1: alice joined the group",
do
bob <## "#biz: biz_1 added alice (Alice @ Biz) to the group (connecting...)"
bob <## "#biz: new member alice is connected"
]
alice #> "#bob hey"
concurrently_
(bob <# "#biz alice> hey")
(biz <# "#bob_1 alice> hey")
bob #> "#biz hey there"
concurrently_
(alice <# "#bob bob_1> hey there")
(biz <# "#bob_1 bob> hey there")
testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () testPlanAddressOkKnown :: HasCallStack => FilePath -> IO ()
testPlanAddressOkKnown = testPlanAddressOkKnown =
testChat2 aliceProfile bobProfile $ testChat2 aliceProfile bobProfile $
@ -2380,7 +2425,7 @@ testSetUITheme =
a <## "you've shared main profile with this contact" a <## "you've shared main profile with this contact"
a <## "connection not verified, use /code command to see security code" a <## "connection not verified, use /code command to see security code"
a <## "quantum resistant end-to-end encryption" a <## "quantum resistant end-to-end encryption"
a <## "peer chat protocol version range: (Version 1, Version 9)" a <## "peer chat protocol version range: (Version 1, Version 10)"
groupInfo a = do groupInfo a = do
a <## "group ID: 1" a <## "group ID: 1"
a <## "current members: 1" a <## "current members: 1"

View file

@ -64,6 +64,9 @@ cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Not
danProfile :: Profile danProfile :: Profile
danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs}
businessProfile :: Profile
businessProfile = Profile {displayName = "biz", fullName = "Biz Inc", image = Nothing, contactLink = Nothing, preferences = defaultPrefs}
it :: HasCallStack => String -> (FilePath -> Expectation) -> SpecWith (Arg (FilePath -> Expectation)) it :: HasCallStack => String -> (FilePath -> Expectation) -> SpecWith (Arg (FilePath -> Expectation))
it name test = it name test =
Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure

View file

@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
it "x.msg.new chat message with chat version range" $ it "x.msg.new chat message with chat version range" $
"{\"v\":\"1-9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" "{\"v\":\"1-10\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
it "x.msg.new quote" $ it "x.msg.new quote" $
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
@ -232,10 +232,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
==# XContact testProfile Nothing ==# XContact testProfile Nothing
it "x.grp.inv" $ it "x.grp.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing, groupSize = Nothing} #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Nothing, groupSize = Nothing}
it "x.grp.inv with group link id" $ it "x.grp.inv with group link id" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing}
it "x.grp.acpt without incognito profile" $ it "x.grp.acpt without incognito profile" $
"{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpAcpt (MemberId "\1\2\3\4") #==# XGrpAcpt (MemberId "\1\2\3\4")
@ -243,13 +243,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile}
it "x.grp.mem.new with member chat version range" $ it "x.grp.mem.new with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile}
it "x.grp.mem.intro" $ it "x.grp.mem.intro" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing
it "x.grp.mem.intro with member chat version range" $ it "x.grp.mem.intro with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing
it "x.grp.mem.intro with member restrictions" $ it "x.grp.mem.intro with member restrictions" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
@ -264,7 +264,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $ it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"