From 42fe94752c07070478bf2d2a8ef45b6b433208e4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:17:27 +0000 Subject: [PATCH] core, ui: public group profile wip (#6734) --- apps/ios/Shared/Model/AppAPITypes.swift | 2 +- .../Views/Chat/Group/GroupChatInfoView.swift | 4 +- apps/ios/SimpleXChat/ChatTypes.swift | 34 ++++++- apps/ios/product/flows/connection.md | 2 +- apps/ios/product/views/group-info.md | 2 +- apps/ios/spec/client/chat-view.md | 4 +- apps/ios/spec/state.md | 2 +- .../chat/simplex/common/model/ChatModel.kt | 35 ++++++- .../chat/simplex/common/model/SimpleXAPI.kt | 2 +- .../views/chat/group/GroupChatInfoView.kt | 7 +- bots/api/TYPES.md | 27 +++++- bots/src/API/Docs/Types.hs | 4 + .../types/typescript/src/types.ts | 17 +++- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Library/Commands.hs | 36 ++++--- src/Simplex/Chat/Library/Internal.hs | 6 +- src/Simplex/Chat/Library/Subscriber.hs | 33 ++++--- src/Simplex/Chat/Store/Connections.hs | 4 +- src/Simplex/Chat/Store/Groups.hs | 94 +++++++++++-------- .../Migrations/M20260222_chat_relays.hs | 12 ++- .../Migrations/M20260222_chat_relays.hs | 12 +-- .../SQLite/Migrations/chat_query_plans.txt | 50 ++++++---- .../Store/SQLite/Migrations/chat_schema.sql | 5 +- src/Simplex/Chat/Store/Shared.hs | 32 ++++--- src/Simplex/Chat/Types.hs | 40 +++++++- tests/ProtocolTests.hs | 2 +- 26 files changed, 323 insertions(+), 147 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 44a03aa037..a08e1ffbc5 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1361,7 +1361,7 @@ enum ContactAddressPlan: Decodable, Hashable { public struct GroupShortLinkInfo: Decodable, Hashable { public var direct: Bool public var groupRelays: [String] - public var sharedGroupId: String? + public var publicGroupId: String? } enum GroupLinkPlan: Decodable, Hashable { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 0bee669488..c02f4dae36 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -106,7 +106,7 @@ struct GroupChatInfoView: View { // TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership) if groupInfo.isOwner && groupLink != nil { channelLinkButton() - } else if let link = groupInfo.groupProfile.groupLink { + } else if let link = groupInfo.groupProfile.publicGroup?.groupLink { SimpleXLinkQRCode(uri: link) Button { showShareSheet(items: [simplexChatLink(link)]) @@ -118,7 +118,7 @@ struct GroupChatInfoView: View { channelMembersButton() } } footer: { - if !groupInfo.isOwner && groupInfo.groupProfile.groupLink != nil { + if !groupInfo.isOwner && groupInfo.groupProfile.publicGroup?.groupLink != nil { Text("You can share a link or a QR code - anybody will be able to join the channel.") .foregroundColor(theme.colors.secondary) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index cfa66218b3..a337837dff 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2432,6 +2432,34 @@ public struct GroupRef: Decodable, Hashable { var localDisplayName: GroupName } +public enum GroupType: Codable, Hashable { + case channel + case unknown(type: String) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + switch type { + case "channel": self = .channel + default: self = .unknown(type: type) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .channel: try container.encode("channel") + case let .unknown(type): try container.encode(type) + } + } +} + +public struct PublicGroupProfile: Codable, Hashable { + public var groupType: GroupType + public var groupLink: String + public var publicGroupId: String +} + public struct GroupProfile: Codable, NamedChat, Hashable { public init( displayName: String, @@ -2439,7 +2467,7 @@ public struct GroupProfile: Codable, NamedChat, Hashable { shortDescr: String? = nil, description: String? = nil, image: String? = nil, - groupLink: String? = nil, + publicGroup: PublicGroupProfile? = nil, groupPreferences: GroupPreferences? = nil, memberAdmission: GroupMemberAdmission? = nil ) { @@ -2448,7 +2476,7 @@ public struct GroupProfile: Codable, NamedChat, Hashable { self.shortDescr = shortDescr self.description = description self.image = image - self.groupLink = groupLink + self.publicGroup = publicGroup self.groupPreferences = groupPreferences self.memberAdmission = memberAdmission } @@ -2458,7 +2486,7 @@ public struct GroupProfile: Codable, NamedChat, Hashable { public var shortDescr: String? public var description: String? public var image: String? - public var groupLink: String? + public var publicGroup: PublicGroupProfile? public var groupPreferences: GroupPreferences? public var memberAdmission: GroupMemberAdmission? public var localAlias: String { "" } diff --git a/apps/ios/product/flows/connection.md b/apps/ios/product/flows/connection.md index 05051141f9..c621dc5124 100644 --- a/apps/ios/product/flows/connection.md +++ b/apps/ios/product/flows/connection.md @@ -138,7 +138,7 @@ Establishing contact between two SimpleX Chat users. SimpleX uses no user identi | `Contact` | `SimpleXChat/ChatTypes.swift` | Full contact model with profile, connection status, preferences | | `UserContactRequest` | `SimpleXChat/ChatTypes.swift` | Incoming contact request awaiting acceptance | | `ChatType` | `SimpleXChat/ChatTypes.swift` | `.direct`, `.group`, `.local`, `.contactRequest`, `.contactConnection` | -| `GroupShortLinkInfo` | `Shared/Model/AppAPITypes.swift` | Contains `direct: Bool`, `groupRelays: [String]`, `sharedGroupId: String?`; transient data returned by prepare | +| `GroupShortLinkInfo` | `Shared/Model/AppAPITypes.swift` | Contains `direct: Bool`, `groupRelays: [String]`, `publicGroupId: String?`; transient data returned by prepare | | `RelayConnectionResult` | `Shared/Model/AppAPITypes.swift` | Contains `relayMember: GroupMember`, `relayError: ChatError?`; per-relay join outcome | ## Error Cases diff --git a/apps/ios/product/views/group-info.md b/apps/ios/product/views/group-info.md index 3ec322ec0e..ee0c449c68 100644 --- a/apps/ios/product/views/group-info.md +++ b/apps/ios/product/views/group-info.md @@ -138,7 +138,7 @@ The top section splits into a channel-specific branch: | Element | Owner | Non-owner | |---|---|---| -| Channel link | NavigationLink "Channel link" to `GroupLinkView` | Inline QR code (`SimpleXLinkQRCode`) + "Share link" button (if `groupProfile.groupLink` exists) | +| Channel link | NavigationLink "Channel link" to `GroupLinkView` | Inline QR code (`SimpleXLinkQRCode`) + "Share link" button (if `groupProfile.publicGroup?.groupLink` exists) | | Members | NavigationLink "Owners & subscribers" to `ChannelMembersView` | NavigationLink "Owners" to `ChannelMembersView` | | Relays | NavigationLink "Chat relays" to `ChannelRelaysView` | NavigationLink "Chat relays" to `ChannelRelaysView` | diff --git a/apps/ios/spec/client/chat-view.md b/apps/ios/spec/client/chat-view.md index 111b8ec1f4..182e7b7ce9 100644 --- a/apps/ios/spec/client/chat-view.md +++ b/apps/ios/spec/client/chat-view.md @@ -322,7 +322,7 @@ When `groupInfo.useRelays == true`, [`GroupChatInfoView`](../../Shared/Views/Cha | Section | Owner | Subscriber | |---------|-------|-----------| -| 1. Links & Members | Channel link (manage via GroupLinkView), Owners & subscribers | Channel link (read-only QR from `groupProfile.groupLink`), Owners | +| 1. Links & Members | Channel link (manage via GroupLinkView), Owners & subscribers | Channel link (read-only QR from `groupProfile.publicGroup?.groupLink`), Owners | | 2. Profile & Welcome | Edit channel profile, Welcome message | Welcome message (if exists) | | 3. Theme & TTL | Chat theme, Delete messages after | Chat theme, Delete messages after | | 4. Actions | Chat relays, Clear chat, Delete channel | Chat relays, Clear chat, Leave channel | @@ -343,7 +343,7 @@ Member rows show profile image, display name (with verified shield), connection ### Channel Link -Owner sees [`channelLinkButton()`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift#L605) (navigates to `GroupLinkView` for full link management), guarded by `groupInfo.isOwner && groupLink != nil` — channel links can only be created during channel creation, not from the info view. A TODO marks the need for protocol changes to allow other owners to manage the same channel link. Non-owner sees read-only QR code displaying `groupProfile.groupLink` via `SimpleXLinkQRCode`. `apiGetGroupLink` is skipped in `onAppear` for non-owner channels. +Owner sees [`channelLinkButton()`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift#L605) (navigates to `GroupLinkView` for full link management), guarded by `groupInfo.isOwner && groupLink != nil` — channel links can only be created during channel creation, not from the info view. A TODO marks the need for protocol changes to allow other owners to manage the same channel link. Non-owner sees read-only QR code displaying `groupProfile.publicGroup?.groupLink` via `SimpleXLinkQRCode`. `apiGetGroupLink` is skipped in `onAppear` for non-owner channels. Groups use separate [`groupLinkButton()`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift#L593) which supports both "Create group link" and "Group link" labels. diff --git a/apps/ios/spec/state.md b/apps/ios/spec/state.md index c989547299..6dda4ba275 100644 --- a/apps/ios/spec/state.md +++ b/apps/ios/spec/state.md @@ -384,7 +384,7 @@ A **channel** is a group with `groupInfo.useRelays == true`. These types support | `User` | `userChatRelay` | `Bool` | Whether user acts as a chat relay | [L46](../SimpleXChat/ChatTypes.swift#L46) | | `GroupInfo` | `useRelays` | `Bool` | Whether group uses relay infrastructure (channel mode) | [L2343](../SimpleXChat/ChatTypes.swift#L2343) | | `GroupInfo` | `relayOwnStatus` | `RelayStatus?` | Current user's relay status in this group | [L2344](../SimpleXChat/ChatTypes.swift#L2344) | -| `GroupProfile` | `groupLink` | `String?` | Group's short link | [L2452](../SimpleXChat/ChatTypes.swift#L2452) | +| `GroupProfile` | `publicGroup` | `PublicGroupProfile?` | Channel-specific profile data (type, link, ID) | [L2472](../SimpleXChat/ChatTypes.swift#L2472) | #### New Types diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 09b026bb30..cd7f55fcb8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2163,6 +2163,39 @@ data class PreparedGroup ( @Serializable data class GroupRef(val groupId: Long, val localDisplayName: String) +@Serializable(with = GroupTypeSerializer::class) +sealed class GroupType { + @Serializable @SerialName("channel") object Channel: GroupType() + @Serializable @SerialName("unknown") data class Unknown(val type: String): GroupType() +} + +object GroupTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("GroupType", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): GroupType { + return when (val value = decoder.decodeString()) { + "channel" -> GroupType.Channel + else -> GroupType.Unknown(value) + } + } + + override fun serialize(encoder: Encoder, value: GroupType) { + val stringValue = when (value) { + is GroupType.Channel -> "channel" + is GroupType.Unknown -> value.type + } + encoder.encodeString(stringValue) + } +} + +@Serializable +data class PublicGroupProfile( + val groupType: GroupType, + val groupLink: String, + val publicGroupId: String +) + @Serializable data class GroupProfile ( override val displayName: String, @@ -2170,7 +2203,7 @@ data class GroupProfile ( override val shortDescr: String?, val description: String? = null, override val image: String? = null, - val groupLink: String? = null, + val publicGroup: PublicGroupProfile? = null, override val localAlias: String = "", val groupPreferences: GroupPreferences? = null, val memberAdmission: GroupMemberAdmission? = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 779209a00f..4ea44aca15 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -4548,7 +4548,7 @@ data class RelayConnectionResult( data class GroupShortLinkInfo( val direct: Boolean, val groupRelays: List, - val sharedGroupId: String? = null + val publicGroupId: String? = null ) @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 1973e5a599..78eb31ccbe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -545,21 +545,22 @@ fun ModalData.GroupChatInfoLayout( } var anyTopSectionRowShow = false + val channelLink = groupInfo.groupProfile.publicGroup?.groupLink if (groupInfo.useRelays) { SectionView { if (groupInfo.isOwner && groupLink != null) { anyTopSectionRowShow = true ChannelLinkButton(manageGroupLink) - } else if (groupInfo.groupProfile.groupLink != null) { + } else if (channelLink != null) { anyTopSectionRowShow = true - ChannelLinkQRCodeSection(groupInfo.groupProfile.groupLink!!) + ChannelLinkQRCodeSection(channelLink) } if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) { anyTopSectionRowShow = true ChannelMembersButton(chat.remoteHostId, groupInfo, showMemberInfo) } } - if (!groupInfo.isOwner && groupInfo.groupProfile.groupLink != null) { + if (!groupInfo.isOwner && channelLink != null) { SectionTextFooter(stringResource(MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect)) } } else { diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 200bc2d769..62f774ac52 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -109,6 +109,7 @@ This file is generated automatically. - [GroupShortLinkInfo](#groupshortlinkinfo) - [GroupSummary](#groupsummary) - [GroupSupportChat](#groupsupportchat) +- [GroupType](#grouptype) - [HandshakeError](#handshakeerror) - [InlineFileMode](#inlinefilemode) - [InvitationLinkPlan](#invitationlinkplan) @@ -138,6 +139,7 @@ This file is generated automatically. - [ProxyClientError](#proxyclienterror) - [ProxyError](#proxyerror) - [PublicGroupData](#publicgroupdata) +- [PublicGroupProfile](#publicgroupprofile) - [RCErrorType](#rcerrortype) - [RatchetSyncState](#ratchetsyncstate) - [RcvConnEvent](#rcvconnevent) @@ -2199,7 +2201,7 @@ MemberSupport: ## GroupKeys **Record type**: -- sharedGroupId: string +- publicGroupId: string - groupRootKey: [GroupRootKey](#grouprootkey) - memberPrivKey: string @@ -2382,10 +2384,9 @@ Known: - shortDescr: string? - description: string? - image: string? -- groupLink: string? +- publicGroup: [PublicGroupProfile](#publicgroupprofile)? - groupPreferences: [GroupPreferences](#grouppreferences)? - memberAdmission: [GroupMemberAdmission](#groupmemberadmission)? -- sharedGroupId: string? --- @@ -2431,7 +2432,7 @@ Public: **Record type**: - direct: bool - groupRelays: [string] -- sharedGroupId: string? +- publicGroupId: string? --- @@ -2455,6 +2456,14 @@ Public: - lastMsgFromMemberTs: UTCTime? +--- + +## GroupType + +**Enum type**: +- "channel" + + --- ## HandshakeError @@ -2915,6 +2924,16 @@ NO_SESSION: - publicMemberCount: int64 +--- + +## PublicGroupProfile + +**Record type**: +- groupType: [GroupType](#grouptype) +- groupLink: string +- publicGroupId: string + + --- ## RCErrorType diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 19a33a678c..f112dff3df 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -291,6 +291,7 @@ chatTypesDocsData = (sti @GroupShortLinkInfo, STRecord, "", [], "", ""), (sti @GroupSummary, STRecord, "", [], "", ""), (sti @GroupSupportChat, STRecord, "", [], "", ""), + (sti @GroupType, STEnum1, "GT", ["GTUnknown"], "", ""), (sti @HandshakeError, STEnum, "", [], "", ""), (sti @InlineFileMode, STEnum, "IFM", [], "", ""), (sti @InvitationLinkPlan, STUnion, "ILP", [], "", ""), @@ -321,6 +322,7 @@ chatTypesDocsData = (sti @ProxyClientError, STUnion, "Proxy", [], "", ""), (sti @ProxyError, STUnion, "", [], "", ""), (sti @PublicGroupData, STRecord, "", [], "", ""), + (sti @PublicGroupProfile, STRecord, "", [], "", ""), (sti @RatchetSyncState, STEnum, "RS", [], "", ""), (sti @RCErrorType, STUnion, "RCE", [], "", ""), (sti @RcvConnEvent, STUnion, "RCE", [], "", ""), @@ -484,6 +486,7 @@ deriving instance Generic GroupProfile deriving instance Generic GroupRelay deriving instance Generic GroupShortLinkData deriving instance Generic GroupShortLinkInfo +deriving instance Generic GroupType deriving instance Generic GroupSummary deriving instance Generic GroupSupportChat deriving instance Generic HandshakeError @@ -522,6 +525,7 @@ deriving instance Generic Profile deriving instance Generic ProxyClientError deriving instance Generic ProxyError deriving instance Generic PublicGroupData +deriving instance Generic PublicGroupProfile deriving instance Generic RatchetSyncState deriving instance Generic RCErrorType deriving instance Generic RcvConnEvent diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index c693fe1888..8930969d27 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2519,7 +2519,7 @@ export interface GroupInfo { } export interface GroupKeys { - sharedGroupId: string + publicGroupId: string groupRootKey: GroupRootKey memberPrivKey: string } @@ -2671,10 +2671,9 @@ export interface GroupProfile { shortDescr?: string description?: string image?: string - groupLink?: string + publicGroup?: PublicGroupProfile groupPreferences?: GroupPreferences memberAdmission?: GroupMemberAdmission - sharedGroupId?: string } export interface GroupRelay { @@ -2713,7 +2712,7 @@ export interface GroupShortLinkData { export interface GroupShortLinkInfo { direct: boolean groupRelays: string[] - sharedGroupId?: string + publicGroupId?: string } export interface GroupSummary { @@ -2729,6 +2728,10 @@ export interface GroupSupportChat { lastMsgFromMemberTs?: string // ISO-8601 timestamp } +export enum GroupType { + Channel = "channel", +} + export enum HandshakeError { PARSE = "PARSE", IDENTITY = "IDENTITY", @@ -3216,6 +3219,12 @@ export interface PublicGroupData { publicMemberCount: number // int64 } +export interface PublicGroupProfile { + groupType: GroupType + groupLink: string + publicGroupId: string +} + export type RCErrorType = | RCErrorType.Internal | RCErrorType.Identity diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d82ffb255f..2a4e10cccf 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1014,7 +1014,7 @@ type DirectLink = Bool data GroupShortLinkInfo = GroupShortLinkInfo { direct :: Bool, groupRelays :: [ShortLinkContact], - sharedGroupId :: Maybe B64UrlByteString + publicGroupId :: Maybe B64UrlByteString } deriving (Show) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d488b6e990..3c9d1fbea2 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1686,9 +1686,9 @@ processChatCommand vr nm = \case APIGroupInfo gId -> withUser $ \user -> CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db vr user gId) APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = GroupProfile {groupLink}} <- withFastStore $ \db -> getGroupInfo db vr user groupId - case groupLink of - Just sLnk | useRelays' gInfo -> do + gInfo@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db vr user groupId + case publicGroup of + Just PublicGroupProfile {groupLink = sLnk} | useRelays' gInfo -> do (_, cData) <- getShortLinkConnReq nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData let publicGroupData_ = groupSLinkData_ >>= \GroupShortLinkData {publicGroupData} -> publicGroupData @@ -2024,9 +2024,11 @@ processChatCommand vr nm = \case Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData - -- Validate link entity ID matches group profile's sharedGroupId (relay groups must have both) - forM_ groupSLinkData_ $ \GroupShortLinkData {groupProfile = GroupProfile {sharedGroupId}} -> - unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ throwChatError CEInvalidConnReq + -- Validate link entity ID matches group profile's publicGroupId (relay groups must have both) + case groupSLinkData_ of + Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {publicGroupId}}} + | (B64UrlByteString <$> linkEntityId) == Just publicGroupId -> pure () + _ -> throwChatError CEInvalidConnReq let publicGroupData_ = groupSLinkData_ >>= \GroupShortLinkData {publicGroupData} -> publicGroupData publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ -- Prepare group record once before connecting to relays (updatePreparedRelayedGroup): @@ -2036,7 +2038,7 @@ processChatCommand vr nm = \case gVar <- asks random (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar gInfo' <- withFastStore $ \db -> do - gInfo' <- updatePreparedRelayedGroup db vr user gInfo mainCReq cReqHash incognitoProfile linkEntityId rootKey memberPrivKey publicMemberCount_ + gInfo' <- updatePreparedRelayedGroup db vr user gInfo mainCReq cReqHash incognitoProfile rootKey memberPrivKey publicMemberCount_ -- Pre-emptively create owner members with trusted keys from link data forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> void $ createLinkOwnerMember db vr user gInfo' (MemberId ownerId) ownerKey @@ -2400,12 +2402,12 @@ processChatCommand vr nm = \case -- generate owner key, OwnerAuth signed by root key memberId <- MemberId <$> liftIO (encodedRandomBytes gVar 12) (memberPrivKey, ownerAuth) <- liftIO $ SL.newOwnerAuth gVar (unMemberId memberId) rootPrivKey - let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk, sharedGroupId = Just $ B64UrlByteString entityId} + let groupProfile' = (groupProfile :: GroupProfile) {publicGroup = Just PublicGroupProfile {groupType = GTChannel, groupLink = sLnk, publicGroupId = B64UrlByteString entityId}} userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)} userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData} -- create connection with prepared link (single network call) connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOff subMode - let groupKeys = GroupKeys {sharedGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} + let groupKeys = GroupKeys {publicGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} setupLink gInfo = do -- TODO [relays] starting role should be communicated in protocol from owner to relays subRole <- asks $ channelSubscriberRole . config @@ -3900,12 +3902,16 @@ processChatCommand vr nm = \case Nothing -> do (fd, cData@(ContactLinkData _ UserContactData {direct, relays})) <- getShortLinkConnReq nm user l' let FixedLinkData {linkConnReq = cReq, linkEntityId} = fd - linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, sharedGroupId = B64UrlByteString <$> linkEntityId} + linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} groupSLinkData_ <- liftIO $ decodeLinkUserData cData - -- Validate link entity ID matches group profile's sharedGroupId - forM_ groupSLinkData_ $ \GroupShortLinkData {groupProfile = GroupProfile {sharedGroupId}} -> - unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ - throwChatError CEInvalidConnReq + -- Cross-validate linkEntityId and publicGroupId from profile: + -- for channels both must be present and match, for p2p groups both must be absent + let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> + fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup + case (B64UrlByteString <$> linkEntityId, profilePGId) of + (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () + (Nothing, Nothing) -> pure () + _ -> throwChatError CEInvalidConnReq plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ pure (con cReq, plan) where @@ -5098,7 +5104,7 @@ chatCommandP = { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } - pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} + pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences, memberAdmission = Nothing} memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) shortDescrP = do descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure "" diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 8ffed9536d..e13b240c8d 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1054,7 +1054,7 @@ acceptRelayJoinRequestAsync businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} + GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1881,9 +1881,9 @@ createSndMessages idsEvents = do encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} groupMsgSigning :: GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning -groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {sharedGroupId, memberPrivKey}} evt +groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {publicGroupId, memberPrivKey}} evt | useRelays' gInfo && requiresSignature (toCMEventTag evt) = - Just $ MsgSigning CBGroup (smpEncode (sharedGroupId, memberId)) KRMember memberPrivKey + Just $ MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey groupMsgSigning _ _ = Nothing sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 1c5727f131..24159934e2 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -743,8 +743,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "CONF from invited member must have x.grp.acpt" GCHostMember -> case chatMsgEvent of - XGrpLinkInv glInv@GroupLinkInvitation {groupProfile = GroupProfile {sharedGroupId = rcvGId}} - | let GroupInfo {groupProfile = GroupProfile {sharedGroupId = curGId}} = gInfo, rcvGId == curGId -> do + XGrpLinkInv glInv@GroupLinkInvitation {groupProfile = GroupProfile {publicGroup = rcvPG}} + | let GroupInfo {groupProfile = GroupProfile {publicGroup = curPG}} = gInfo + pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId), + useRelays' gInfo == isJust rcvPG && pgId rcvPG == pgId curPG -> do -- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db vr user gInfo m glInv -- [incognito] send saved profile @@ -752,7 +754,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let profileToSend = userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) allowAgentConnectionAsync user conn' confId $ XInfo profileToSend toView $ CEvtGroupLinkConnecting user gInfo' m' - | otherwise -> messageError "x.grp.link.inv: sharedGroupId mismatch" + | otherwise -> messageError "x.grp.link.inv: publicGroupId mismatch" XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersRejected db vr user gInfo m glRjct toView $ CEvtGroupLinkConnecting user gInfo' m' @@ -2315,7 +2317,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta - | isJust groupLink || isJust sharedGroupId = messageError "x.grp.inv: can't invite to channel" + | isJust publicGroup = messageError "x.grp.inv: can't invite to channel" | otherwise = do let Contact {localDisplayName = c, activeConn} = ct GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv @@ -2344,7 +2346,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} where - GroupInvitation {groupProfile = GroupProfile {groupLink, sharedGroupId}} = inv + GroupInvitation {groupProfile = GroupProfile {publicGroup}} = inv brokerTs = metaBrokerTs msgMeta sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool sameGroupLinkId (Just gli) (Just gli') = gli == gli' @@ -3077,10 +3079,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {sharedGroupId = gId}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {sharedGroupId = gId'} msg@RcvMessage {msgSigned} brokerTs + xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg'} msg@RcvMessage {msgSigned} brokerTs | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing - | useRelays' g && gId' /= gId = messageError "x.grp.info: sharedGroupId cannot be changed" $> Nothing - | not (useRelays' g) && isJust gId' = messageError "x.grp.info: sharedGroupId not allowed in p2p groups" $> Nothing + | let pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId), + useRelays' g && (isNothing pg' || pgId pg' /= pgId pg) = messageError "x.grp.info: publicGroupId mismatch for channel" $> Nothing + | not (useRelays' g) && isJust pg' = messageError "x.grp.info: publicGroup not allowed in p2p groups" $> Nothing | otherwise = do case businessChat of Nothing -> unless (p == p') $ do @@ -3258,8 +3261,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup | Just GroupKeys {sharedGroupId} <- groupKeys gInfo -> - let prefix = smpEncode chatBinding <> smpEncode (sharedGroupId, memberId) + CBGroup | Just GroupKeys {publicGroupId} <- groupKeys gInfo -> + let prefix = smpEncode chatBinding <> smpEncode (publicGroupId, memberId) in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) @@ -3633,14 +3636,16 @@ runRelayRequestWorker a Worker {doWork} = do (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" - Just GroupShortLinkData {groupProfile = gp@GroupProfile {sharedGroupId}} -> do - unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ - throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match profile sharedGroupId" + Just GroupShortLinkData {groupProfile = gp@GroupProfile {publicGroup}} -> do + pg <- case (linkEntityId, publicGroup) of + (Just entityId, Just pg@PublicGroupProfile {publicGroupId}) + | B64UrlByteString entityId == publicGroupId -> pure pg + _ -> throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match profile publicGroupId" validateGroupProfile gp ((_, memberPrivKey), sLnk) <- createRelayLink gInfo gInfo' <- withStore $ \db -> do void $ updateGroupProfile db user gInfo gp - updateRelayGroupKeys db user gInfo linkEntityId rootKey memberPrivKey owners + updateRelayGroupKeys db user gInfo pg rootKey memberPrivKey owners getGroupInfo db vr user groupId pure (gInfo', sLnk) where diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 348b5b8857..62183a0313 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -138,14 +138,14 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, 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, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index af4b95c3df..0ac0008598 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -340,28 +340,32 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC -- | creates completely new group with a single member - the current user createNewGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys publicMemberCount_ = ExceptT $ do - let GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} = groupProfile + let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile + (groupType_, groupLink_, publicGroupId_) = case publicGroup of + Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) + Nothing -> (Nothing, Nothing, Nothing) fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do - let (sharedGroupId_, rootPrivKey_, rootPubKey_, memberPrivKey_) = case groupKeys of - Nothing -> (Nothing, Nothing, Nothing, Nothing) - Just GroupKeys {sharedGroupId, groupRootKey, memberPrivKey} -> + let (rootPrivKey_, rootPubKey_, memberPrivKey_) = case groupKeys of + Nothing -> (Nothing, Nothing, Nothing) + Just GroupKeys {groupRootKey, memberPrivKey} -> let (rpk, rpub) = case groupRootKey of GRKPrivate pk -> (Just pk, Nothing) GRKPublic k -> (Nothing, Just k) - in (Just sharedGroupId, rpk, rpub, Just memberPrivKey) + in (rpk, rpub, Just memberPrivKey) groupId <- liftIO $ do DB.execute db [sql| INSERT INTO group_profiles - (display_name, full_name, short_descr, description, image, group_link, + (display_name, full_name, short_descr, description, image, + group_type, group_link, public_group_id, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((displayName, fullName, shortDescr, description, image, groupLink) + ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) profileId <- insertedRowId db DB.execute @@ -370,11 +374,11 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - shared_group_id, root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) - :. (sharedGroupId_, rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_) + :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_) ) insertedRowId db let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys @@ -839,18 +843,22 @@ createGroupViaLink' createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe (CreatedLinkContact, Maybe SharedMsgId) -> Maybe BusinessChatInfo -> Bool -> Maybe RelayStatus -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (GroupId, Text) createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus publicMemberCount_ currentTs = ExceptT $ do - let GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} = groupProfile + let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile + (groupType_, groupLink_, publicGroupId_) = case publicGroup of + Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) + Nothing -> (Nothing, Nothing, Nothing) withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do liftIO $ do DB.execute db [sql| INSERT INTO group_profiles - (display_name, full_name, short_descr, description, image, group_link, + (display_name, full_name, short_descr, description, image, + group_type, group_link, public_group_id, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((displayName, fullName, shortDescr, description, image, groupLink) + ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) profileId <- insertedRowId db DB.execute @@ -1508,10 +1516,9 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe shortDescr = Nothing, description = Nothing, image = Nothing, - groupLink = Nothing, + publicGroup = Nothing, groupPreferences = Nothing, - memberAdmission = Nothing, - sharedGroupId = Nothing + memberAdmission = Nothing } (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs -- Store relay request data for recovery @@ -1775,13 +1782,13 @@ createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentCon -- which is used in single-connection flows. updatePreparedRelayedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Profile -> - Maybe ByteString -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> Maybe Int64 -> + C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> Maybe Int64 -> ExceptT StoreError IO GroupInfo -updatePreparedRelayedGroup db vr user@User {userId} gInfo cReq cReqHash incognitoProfile linkEntityId rootPubKey memberPrivKey publicMemberCount_ = do +updatePreparedRelayedGroup db vr user@User {userId} gInfo cReq cReqHash incognitoProfile rootPubKey memberPrivKey publicMemberCount_ = do currentTs <- liftIO getCurrentTime customUserProfileId <- liftIO $ mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile liftIO $ setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId publicMemberCount_ currentTs - liftIO $ updateGroupMemberKeys db (groupId' gInfo) linkEntityId rootPubKey memberPrivKey (groupMemberId' $ membership gInfo) + liftIO $ updateGroupMemberKeys db (groupId' gInfo) rootPubKey memberPrivKey (groupMemberId' $ membership gInfo) getGroupInfo db vr user (groupId' gInfo) updatePublicMemberCount :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO GroupInfo @@ -1809,27 +1816,35 @@ setPublicMemberCount db vr user GroupInfo {groupId} publicCount = do liftIO $ DB.execute db "UPDATE groups SET public_member_count = ?, updated_at = ? WHERE group_id = ?" (publicCount, currentTs, groupId) getGroupInfo db vr user groupId -updateGroupMemberKeys :: DB.Connection -> GroupId -> Maybe ByteString -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> GroupMemberId -> IO () -updateGroupMemberKeys db groupId linkEntityId rootPubKey memberPrivKey membershipGMId = do +updateGroupMemberKeys :: DB.Connection -> GroupId -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> GroupMemberId -> IO () +updateGroupMemberKeys db groupId rootPubKey memberPrivKey membershipGMId = do currentTs <- getCurrentTime DB.execute db - "UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" - (Binary <$> linkEntityId, rootPubKey, memberPrivKey, currentTs, groupId) + "UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" + (rootPubKey, memberPrivKey, currentTs, groupId) DB.execute db "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" (C.publicKey memberPrivKey, currentTs, membershipGMId) -updateRelayGroupKeys :: DB.Connection -> User -> GroupInfo -> Maybe ByteString -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> [OwnerAuth] -> ExceptT StoreError IO () -updateRelayGroupKeys db user gInfo linkEntityId rootPubKey memberPrivKey owners = do +updateRelayGroupKeys :: DB.Connection -> User -> GroupInfo -> PublicGroupProfile -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> [OwnerAuth] -> ExceptT StoreError IO () +updateRelayGroupKeys db user@User {userId} gInfo PublicGroupProfile {groupType, groupLink, publicGroupId} rootPubKey memberPrivKey owners = do currentTs <- liftIO getCurrentTime let membershipGMId = groupMemberId' $ membership gInfo + groupId = groupId' gInfo liftIO $ do DB.execute db - "UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" - (Binary <$> linkEntityId, rootPubKey, memberPrivKey, currentTs, groupId' gInfo) + [sql| + UPDATE group_profiles SET group_type = ?, group_link = ?, public_group_id = ?, updated_at = ? + WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?) + |] + (groupType, groupLink, publicGroupId, currentTs, userId, groupId) + DB.execute + db + "UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" + (rootPubKey, memberPrivKey, currentTs, groupId) DB.execute db "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" @@ -2207,7 +2222,7 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs @@ -2220,21 +2235,24 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences} where fullGroupPreferences = mergeGroupPreferences groupPreferences + (groupType_, groupLink_) = case publicGroup of + Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink) + Nothing -> (Nothing, Nothing) updateGroupProfile_ currentTs = DB.execute db [sql| UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_link = ?, preferences = ?, member_admission = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + group_type = ?, group_link = ?, + preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - ( (newName, fullName, shortDescr, description, image, groupLink) - :. (groupPreferences, memberAdmission, currentTs, userId, groupId) - ) + ((newName, fullName, shortDescr, description, image, groupType_, groupLink_) :. (groupPreferences, memberAdmission, currentTs, userId, groupId)) updateGroup_ ldn currentTs = do DB.execute db @@ -2272,14 +2290,16 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName DB.query db [sql| - SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_link, gp.preferences, gp.member_admission + SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, + gp.group_type, gp.group_link, gp.public_group_id, + gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? |] (Only groupId) - toGroupProfile (displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission) = - GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission, sharedGroupId = Nothing} + toGroupProfile (displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_, groupPreferences, memberAdmission) = + GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_, groupPreferences, memberAdmission} getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs index 1d39060d07..0bc6d03e3f 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs @@ -39,13 +39,15 @@ ALTER TABLE groups ADD COLUMN relay_request_peer_chat_max_version INTEGER, ADD COLUMN relay_request_failed SMALLINT DEFAULT 0, ADD COLUMN relay_request_err_reason TEXT, - ADD COLUMN shared_group_id BYTEA, ADD COLUMN root_priv_key BYTEA, ADD COLUMN root_pub_key BYTEA, ADD COLUMN member_priv_key BYTEA, ADD COLUMN public_member_count BIGINT; -ALTER TABLE group_profiles ADD COLUMN group_link BYTEA; +ALTER TABLE group_profiles + ADD COLUMN group_type TEXT, + ADD COLUMN group_link BYTEA, + ADD COLUMN public_group_id BYTEA; CREATE TABLE group_relays( group_relay_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, @@ -90,13 +92,15 @@ ALTER TABLE groups DROP COLUMN relay_request_peer_chat_max_version, DROP COLUMN relay_request_failed, DROP COLUMN relay_request_err_reason, - DROP COLUMN shared_group_id, DROP COLUMN root_priv_key, DROP COLUMN root_pub_key, DROP COLUMN member_priv_key, DROP COLUMN public_member_count; -ALTER TABLE group_profiles DROP COLUMN group_link; +ALTER TABLE group_profiles + DROP COLUMN group_type, + DROP COLUMN group_link, + DROP COLUMN public_group_id; DROP INDEX idx_group_relays_group_id; DROP INDEX idx_group_relays_group_member_id; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs index 250cf25bd6..fe8c79b1a5 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs @@ -41,24 +41,22 @@ CREATE UNIQUE INDEX idx_chat_relays_user_id_name ON chat_relays(user_id, name); ALTER TABLE users ADD COLUMN is_user_chat_relay INTEGER NOT NULL DEFAULT 0; ALTER TABLE groups ADD COLUMN use_relays INTEGER NOT NULL DEFAULT 0; - ALTER TABLE groups ADD COLUMN creating_in_progress INTEGER NOT NULL DEFAULT 0; - ALTER TABLE groups ADD COLUMN relay_own_status TEXT; - ALTER TABLE groups ADD COLUMN relay_request_inv_id BLOB; ALTER TABLE groups ADD COLUMN relay_request_group_link BLOB; ALTER TABLE groups ADD COLUMN relay_request_peer_chat_min_version INTEGER; ALTER TABLE groups ADD COLUMN relay_request_peer_chat_max_version INTEGER; ALTER TABLE groups ADD COLUMN relay_request_failed INTEGER DEFAULT 0; ALTER TABLE groups ADD COLUMN relay_request_err_reason TEXT; -ALTER TABLE groups ADD COLUMN shared_group_id BLOB; ALTER TABLE groups ADD COLUMN root_priv_key BLOB; ALTER TABLE groups ADD COLUMN root_pub_key BLOB; ALTER TABLE groups ADD COLUMN member_priv_key BLOB; ALTER TABLE groups ADD COLUMN public_member_count INTEGER; +ALTER TABLE group_profiles ADD COLUMN group_type TEXT; ALTER TABLE group_profiles ADD COLUMN group_link BLOB; +ALTER TABLE group_profiles ADD COLUMN public_group_id BLOB; CREATE TABLE group_relays( group_relay_id INTEGER PRIMARY KEY, @@ -92,24 +90,22 @@ UPDATE group_members SET member_role = 'observer' WHERE member_role = 'relay'; ALTER TABLE users DROP COLUMN is_user_chat_relay; ALTER TABLE groups DROP COLUMN use_relays; - ALTER TABLE groups DROP COLUMN creating_in_progress; - ALTER TABLE groups DROP COLUMN relay_own_status; - ALTER TABLE groups DROP COLUMN relay_request_inv_id; ALTER TABLE groups DROP COLUMN relay_request_group_link; ALTER TABLE groups DROP COLUMN relay_request_peer_chat_min_version; ALTER TABLE groups DROP COLUMN relay_request_peer_chat_max_version; ALTER TABLE groups DROP COLUMN relay_request_failed; ALTER TABLE groups DROP COLUMN relay_request_err_reason; -ALTER TABLE groups DROP COLUMN shared_group_id; ALTER TABLE groups DROP COLUMN root_priv_key; ALTER TABLE groups DROP COLUMN root_pub_key; ALTER TABLE groups DROP COLUMN member_priv_key; ALTER TABLE groups DROP COLUMN public_member_count; +ALTER TABLE group_profiles DROP COLUMN group_type; ALTER TABLE group_profiles DROP COLUMN group_link; +ALTER TABLE group_profiles DROP COLUMN public_group_id; DROP INDEX idx_group_relays_group_id; DROP INDEX idx_group_relays_group_member_id; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 7837b380f6..1a2dfc90e2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -142,14 +142,14 @@ SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, 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, @@ -967,7 +967,9 @@ Plan: SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? AND worker_scope=? AND failed=? AND task_status=?) Query: - SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_link, gp.preferences, gp.member_admission + SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, + gp.group_type, gp.group_link, gp.public_group_id, + gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? @@ -1205,9 +1207,10 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: INSERT INTO group_profiles - (display_name, full_name, short_descr, description, image, group_link, + (display_name, full_name, short_descr, description, image, + group_type, group_link, public_group_id, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1224,8 +1227,8 @@ Query: INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - shared_group_id, root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1736,7 +1739,9 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_link = ?, preferences = ?, member_admission = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + group_type = ?, group_link = ?, + preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups @@ -3957,6 +3962,15 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_profiles SET group_type = ?, group_link = ?, public_group_id = ?, updated_at = ? + WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET member_index = member_index + 1 @@ -5171,14 +5185,14 @@ SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_me Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, @@ -5207,14 +5221,14 @@ SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, @@ -5236,14 +5250,14 @@ SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, @@ -7065,14 +7079,14 @@ Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET send_rcpts = NULL Plan: SCAN groups -Query: UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ? -Plan: -SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 4ab9a442f0..92cdc872cb 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -123,7 +123,9 @@ CREATE TABLE group_profiles( description TEXT NULL, member_admission TEXT, short_descr TEXT, - group_link BLOB + group_type TEXT, + group_link BLOB, + public_group_id BLOB ) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -168,7 +170,6 @@ CREATE TABLE groups( relay_request_peer_chat_max_version INTEGER, relay_request_failed INTEGER DEFAULT 0, relay_request_err_reason TEXT, - shared_group_id BLOB, root_priv_key BLOB, root_pub_key BLOB, member_priv_key BLOB, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index c7031528ac..762cf8469a 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -663,22 +663,22 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupKeysRow = (Maybe B64UrlByteString, Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) +type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe ShortLinkContact) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupLink) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences - groupKeys = toGroupKeys groupKeysRow - sharedGroupId = (\GroupKeys {sharedGroupId = gId} -> gId) <$> groupKeys - groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink, sharedGroupId} + publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ + groupKeys = toGroupKeys publicGroupId_ groupKeysRow + groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} @@ -690,12 +690,16 @@ toPreparedGroup = \case Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkPreparedConnection, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId} _ -> Nothing -toGroupKeys :: GroupKeysRow -> Maybe GroupKeys -toGroupKeys = \case - (Just sharedGroupId, rootPrivKey_, rootPubKey_, Just memberPrivKey) -> - (\grk -> GroupKeys {sharedGroupId, groupRootKey = grk, memberPrivKey}) - <$> (GRKPrivate <$> rootPrivKey_ <|> GRKPublic <$> rootPubKey_) - _ -> Nothing +toPublicGroupProfile :: Maybe GroupType -> Maybe ShortLinkContact -> Maybe B64UrlByteString -> Maybe PublicGroupProfile +toPublicGroupProfile (Just groupType) (Just groupLink) (Just publicGroupId) = + Just PublicGroupProfile {groupType, groupLink, publicGroupId} +toPublicGroupProfile _ _ _ = Nothing + +toGroupKeys :: Maybe B64UrlByteString -> GroupKeysRow -> Maybe GroupKeys +toGroupKeys (Just publicGroupId) (rootPrivKey_, rootPubKey_, Just memberPrivKey) = + (\grk -> GroupKeys {publicGroupId, groupRootKey = grk, memberPrivKey}) + <$> (GRKPrivate <$> rootPrivKey_ <|> GRKPublic <$> rootPubKey_) +toGroupKeys _ _ = Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) = @@ -755,14 +759,14 @@ groupInfoQueryFields = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, - g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, + g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index a30fe15244..ebe4820a11 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -458,7 +458,7 @@ groupRootPubKey (GRKPrivate pk) = C.publicKey pk groupRootPubKey (GRKPublic pk) = pk data GroupKeys = GroupKeys - { sharedGroupId :: B64UrlByteString, + { publicGroupId :: B64UrlByteString, groupRootKey :: GroupRootKey, memberPrivKey :: C.PrivateKeyEd25519 } @@ -755,16 +755,39 @@ fromLocalProfile :: LocalProfile -> Profile fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} = Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} +data GroupType + = GTChannel + | GTUnknown Text + deriving (Eq, Show) + +instance TextEncoding GroupType where + textEncode = \case + GTChannel -> "channel" + GTUnknown tag -> tag + textDecode s = Just $ case s of + "channel" -> GTChannel + tag -> GTUnknown tag + +instance FromField GroupType where fromField = fromTextField_ textDecode + +instance ToField GroupType where toField = toField . textEncode + +data PublicGroupProfile = PublicGroupProfile + { groupType :: GroupType, + groupLink :: ShortLinkContact, + publicGroupId :: B64UrlByteString -- group identity = sha256(genesis root key), immutable + } + deriving (Eq, Show) + data GroupProfile = GroupProfile { displayName :: GroupName, fullName :: Text, shortDescr :: Maybe Text, -- short description limited to 160 characters description :: Maybe Text, -- this has been repurposed as welcome message image :: Maybe ImageData, - groupLink :: Maybe ShortLinkContact, + publicGroup :: Maybe PublicGroupProfile, groupPreferences :: Maybe GroupPreferences, - memberAdmission :: Maybe GroupMemberAdmission, - sharedGroupId :: Maybe B64UrlByteString -- group identity = sha256(genesis root key), immutable + memberAdmission :: Maybe GroupMemberAdmission } deriving (Eq, Show) @@ -2009,6 +2032,15 @@ instance ToField GroupMemberAdmission where instance FromField GroupMemberAdmission where fromField = fromTextField_ decodeJSON +instance FromJSON GroupType where + parseJSON = textParseJSON "GroupType" + +instance ToJSON GroupType where + toJSON = textToJSON + toEncoding = textToEncoding + +$(JQ.deriveJSON defaultJSON ''PublicGroupProfile) + $(JQ.deriveJSON defaultJSON ''GroupProfile) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "IB") ''InvitedBy) diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index da6c3b9026..1b708a2ffa 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -107,7 +107,7 @@ testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile -testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, groupLink = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} +testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do