core, ui: invite member of one group to another group (#7682)

* rfc: invite member of one group to another group

* update

* update

* implement

* version

* update

* do not show error if contact was removed when joining group

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-10-11 09:50:42 +01:00
committed by GitHub
co-authored by Evgeny @ SimpleX Chat
parent 1053e9fb5c
commit 07c363849b
41 changed files with 1571 additions and 52 deletions
+2
View File
@@ -232,6 +232,8 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o
`x.grp.inv` message is sent to invite contact to the group via contact's direct connection and includes group member connection address. This message MUST only be sent by members with `admin` or `owner` role. Optional `groupLinkId` is included when this message is sent to contacts connected via the user's group link. This identifier is a random byte sequence, with no global or even local uniqueness - it is only used for the user's invitations to a given group to provide confirmation to the contact that the group invitation is for the same group the contact was connecting to via the group link, so that the invitation can be automatically accepted by the contact - the contact compares it with the group link id contained in the group link uri's data field.
`x.grp.inv` message can also be sent to a group member via the member connection, to invite the member to another group. In this case, the message MUST only be sent when direct messages are allowed for the sender in the group of the member connection, and receiving clients MUST ignore it otherwise.
`x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user. It optionally includes the invited member's group profile. The inviting user replies with `x.grp.mem.info` that includes the inviting member's group profile.
`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member.
@@ -0,0 +1,204 @@
# Inviting group members to other groups
## Problem
A group admin can invite only a contact to a group.
To invite a member of group A to group B, the admin first creates a contact with this member:
1. Create a member contact (`APICreateMemberContact`).
2. Send the contact invitation in group A (`x.grp.direct.inv`).
3. Wait until the contact connection is established.
4. Invite the contact to group B (`APIAddMember`, `x.grp.inv`).
Steps 1-3 are possible only when direct messages are allowed in group A.
A direct chat is created with each invited member.
## Design
An admin of groups A and B invites a member of group A to group B.
The invitation is sent as `x.grp.inv` over the connection with this member in group A.
Only the invited member receives the invitation.
The invited member sees it in group A and joins group B in the same way as after an invitation from a contact.
### Protocol
`x.grp.inv` is accepted on group member connections, in addition to contact connections.
```
event x.grp.inv
params groupInvitation, unchanged
connection connection with the invited member in group A
forwarding excluded
group history excluded
```
Sender requirements:
```
group A preferences direct messages allowed for the user's role
role in group A author or above
role in group B admin or owner, and not lower than the invited role
group A group, not channel, different from group B
group B group, not channel
membership in A main profile
membership in B main profile
invited member current member with an active connection
invited member version 22 or later
```
The receiving client ignores the invitation when:
```
group A channel
group B channel
group A preferences direct messages not allowed for the sender's role
sender blocked
fromMember role below admin, or below the invited role
fromMember id equal to the invited member id
```
### Sending
New command, next to `APIAddMember`:
```haskell
APIAddMemberViaGroup
{ groupId :: GroupId,
viaGroupId :: GroupId,
groupMemberId :: GroupMemberId,
memberRole :: GroupMemberRole
}
```
```
groupId group B
viaGroupId group A
groupMemberId member of group A
memberRole role in group B
```
Syntax: `/_add #<groupId> #<viaGroupId> <groupMemberId> <memberRole>`
For a member outside group A, `SEGroupMemberNotFound` is returned.
New response, next to `CRSentGroupInvitation`:
```haskell
CRSentGroupInvitationViaGroup
{ user :: User,
groupInfo :: GroupInfo,
member :: GroupMember,
viaGroupInfo :: GroupInfo,
viaMember :: GroupMember
}
```
```
groupInfo group B
member record of the invited member in group B
viaGroupInfo group A
viaMember member of group A
```
In group B, the record of the invited member is linked to the member of group A.
Until the member joins, the same invitation is re-sent on a repeated command.
After the member joins, `CEGroupDuplicateMember` is returned.
After the member leaves, is removed or is rejected, a new invitation is sent.
The sent invitation is saved in group A as `CISndGroupInvitation`, visible to the sender only.
The name of the member in group A is stored in the item (`CIGroupInvitation.localDisplayName`), as the contact name in direct chats.
### Receiving
New event, next to `CEvtReceivedGroupInvitation`:
```haskell
CEvtReceivedGroupInvitationViaGroup
{ user :: User,
groupInfo :: GroupInfo,
viaGroupInfo :: GroupInfo,
viaMember :: GroupMember,
fromMemberRole :: GroupMemberRole,
memberRole :: GroupMemberRole
}
```
```
groupInfo group B
viaGroupInfo group A
viaMember sender, member of group A
fromMemberRole sender role in group B
memberRole invited role in group B
```
```
group B created with status "invited", or found by the invitation link
chat item CIRcvGroupInvitation in group A, main scope, from the sender
```
With automatic acceptance of group invitations enabled, the client joins group B and saves the item with status "accepted".
With an incognito membership in group A, the same incognito profile is used in group B.
In group B, copies of the group A profiles are stored for the sender and for the invited member.
Badge proofs bound to group A are dropped from the copies.
The copies are replaced by the profiles sent in the connection handshake (`x.grp.acpt`, `x.grp.mem.info`).
### Joining
The invited member joins group B with `APIJoinGroup`, or by tapping the invitation in group A.
Status of the invitation item:
```
pending received
accepted joined
rejected group B deleted before joining
```
With disappearing messages on in group A, the item disappears after the status changes, as in direct chats.
The connection with the host is created from the invitation link, independently of the inviter's connection.
So an invitation from a contact deleted before joining is accepted in the same way.
Before this change, joining failed with `SEContactNotFoundByMemberId`.
### Clients
```
group A existing group invitation card ("Tap to join"), with a label
chat list group B, added on CEvtReceivedGroupInvitationViaGroup
sending API only
```
Labels, on group invitation cards in group chats only:
```
received card For you only
sent card For <name> only name of the member in group A
```
### Bots API
```
APIAddMemberViaGroup new command
CRSentGroupInvitationViaGroup new response
CEvtReceivedGroupInvitationViaGroup new event
```
`APIAddMember`, `CRSentGroupInvitation` and `CEvtReceivedGroupInvitation`: unchanged.
### Version
```
chat version 22
```
Version 22 is shared with owner-signed relay invitations, in one constant `groupInvVersion`.
Version 22 is not in any release: v7.1.0-beta.6 uses version 21.
## Out of scope
- Role changes of members invited via a group before they join.
- "Open" action on accepted invitations.
- Invitations to members pending approval in group A.
- User interface for sending invitations.