fix: link group invitations to join requests
This commit is contained in:
@@ -419,16 +419,37 @@ func (a *adapter) TenantGroupInvitationCreate(id t.TenantID, inv *t.GroupInvitat
|
||||
if inv.ExpiresAt.IsZero() {
|
||||
inv.ExpiresAt = now.Add(7 * 24 * time.Hour)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_invitations(invitation_id,topic,inviter_user_id,target_user_id,message,status,result,join_request_id,idempotency_key,expires_at,handled_at,created_at,updated_at)
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_invitations(invitation_id,topic,inviter_user_id,target_user_id,message,status,result,join_request_id,idempotency_key,expires_at,handled_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
inv.InvitationID, inv.Topic, decodeUID(inv.InviterUserID), decodeUID(inv.TargetUserID), inv.Message, inv.Status, inv.Result, inv.JoinRequestID, inv.IdempotencyKey, inv.ExpiresAt, inv.HandledAt, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(inv.JoinRequestID) != "" {
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_join_requests(request_id,topic,applicant_user_id,inviter_user_id,source,qr_token_hash,message,status,reject_reason,expires_at,idempotency_key,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
inv.JoinRequestID, inv.Topic, decodeUID(inv.TargetUserID), nullableUID(inv.InviterUserID), "invitation", "", inv.Message, t.GroupJoinRequestStatusPending, "", inv.ExpiresAt, inv.IdempotencyKey, 1, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = insertGroupEventTx(tx, inv.Topic, inv.TargetUserID, "group.join_request.created", map[string]any{
|
||||
"request_id": inv.JoinRequestID, "applicant_user_id": inv.TargetUserID.UserId(), "source": "invitation", "invitation_id": inv.InvitationID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
@@ -509,6 +530,14 @@ func (a *adapter) TenantGroupInvitationAccept(id t.TenantID, invitationID string
|
||||
t.GroupInvitationStatusJoined, "joined", now, now, inv.InvitationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(inv.JoinRequestID) != "" {
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_join_requests
|
||||
SET status=?,handled_by=?,handled_at=?,updated_at=?,version=version+1
|
||||
WHERE topic=? AND request_id=? AND status=?`,
|
||||
t.GroupJoinRequestStatusAccepted, decodeUID(uid), now, now, inv.Topic, inv.JoinRequestID, t.GroupJoinRequestStatusPending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = insertGroupEventTx(tx, inv.Topic, uid, "group.member.joined", map[string]any{
|
||||
"user_id": uid.UserId(), "source": "invitation", "invitation_id": inv.InvitationID,
|
||||
}); err != nil {
|
||||
|
||||
+16
-5
@@ -5297,12 +5297,12 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
return
|
||||
}
|
||||
access := selfSub.ModeGiven & selfSub.ModeWant
|
||||
settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if !access.IsAdmin() {
|
||||
settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if settings == nil || !settings.MemberInvite {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "member invitation is disabled", nil)
|
||||
return
|
||||
@@ -5351,6 +5351,10 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
} else {
|
||||
idempotencyKey = invitationID
|
||||
}
|
||||
joinRequestID := ""
|
||||
if settings != nil && settings.JoinApproval {
|
||||
joinRequestID = "gjr_" + store.Store.GetUidString()
|
||||
}
|
||||
inv, err := store.GroupBusiness.CreateInvitation(principal.TenantID, &types.GroupInvitation{
|
||||
InvitationID: invitationID,
|
||||
Topic: topic,
|
||||
@@ -5359,6 +5363,7 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
Message: strings.TrimSpace(body.Message),
|
||||
Status: types.GroupInvitationStatusPending,
|
||||
Result: "",
|
||||
JoinRequestID: joinRequestID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
ExpiresAt: now.Add(7 * 24 * time.Hour),
|
||||
CreatedAt: now,
|
||||
@@ -5373,6 +5378,9 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
item["invitation_id"] = inv.InvitationID
|
||||
item["status"] = inv.Status
|
||||
item["expires_at"] = inv.ExpiresAt.Format(time.RFC3339Nano)
|
||||
if inv.JoinRequestID != "" {
|
||||
item["join_request_id"] = inv.JoinRequestID
|
||||
}
|
||||
if imReliableHTTPEventsEnabled(req) {
|
||||
publishIMConversationSystemNotice(principal.TenantID, principal.Uid.P2PName(uid),
|
||||
"system_action", "group_invitation_created", "邀请你加入群聊。",
|
||||
@@ -7109,6 +7117,9 @@ func groupInvitationPayload(inv *types.GroupInvitation) map[string]any {
|
||||
"result": inv.Result,
|
||||
"expires_at": inv.ExpiresAt.Format(time.RFC3339Nano),
|
||||
}
|
||||
if inv.JoinRequestID != "" {
|
||||
data["join_request_id"] = inv.JoinRequestID
|
||||
}
|
||||
if inv.HandledAt != nil {
|
||||
data["handled_at"] = inv.HandledAt.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
@@ -426,6 +426,23 @@ func (s *testGroupBusinessStore) UpdateMember(_ types.TenantID, topic string, ui
|
||||
|
||||
func (s *testGroupBusinessStore) CreateInvitation(_ types.TenantID, inv *types.GroupInvitation) (*types.GroupInvitation, error) {
|
||||
s.invitations = append(s.invitations, *inv)
|
||||
if inv.JoinRequestID != "" {
|
||||
expiresAt := inv.ExpiresAt
|
||||
s.joinRequests = append(s.joinRequests, types.GroupJoinRequest{
|
||||
RequestID: inv.JoinRequestID,
|
||||
Topic: inv.Topic,
|
||||
ApplicantUserID: inv.TargetUserID,
|
||||
InviterUserID: inv.InviterUserID,
|
||||
Source: "invitation",
|
||||
Message: inv.Message,
|
||||
Status: types.GroupJoinRequestStatusPending,
|
||||
ExpiresAt: &expiresAt,
|
||||
IdempotencyKey: inv.IdempotencyKey,
|
||||
Version: 1,
|
||||
CreatedAt: inv.CreatedAt,
|
||||
UpdatedAt: inv.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
@@ -4722,6 +4739,80 @@ func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore.EXPECT().GetUidString().Return("invite")
|
||||
mockStore.EXPECT().GetUidString().Return("request")
|
||||
oldSubs := store.Subs
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
groupStore := &testGroupBusinessStore{settings: &types.GroupSettings{
|
||||
Topic: "grpAlpha", OwnerUserID: types.Uid(42), Name: "Alpha", GroupStatus: types.GroupStatusActive,
|
||||
JoinApproval: true, MemberInvite: true, MaxAdminCount: groupMaxAdminCount, Version: 1,
|
||||
}}
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = groupStore
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
}()
|
||||
owner := types.Uid(42)
|
||||
target := types.Uid(43)
|
||||
ownerSub := &types.Subscription{
|
||||
TenantID: 7, User: owner.String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull,
|
||||
}
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", owner, false).Return(ownerSub, nil).Times(3)
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", target, false).Return(nil, types.ErrNotFound)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/invitations",
|
||||
bytes.NewBufferString(`{"user_ids":["`+target.UserId()+`"],"message":"welcome"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "invite-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
item := items[0].(map[string]any)
|
||||
if item["invitation_id"] != "ginv_invite" || item["join_request_id"] != "gjr_request" || item["status"] != types.GroupInvitationStatusPending {
|
||||
t.Fatalf("unexpected invitation response: %#v", data)
|
||||
}
|
||||
if len(groupStore.joinRequests) != 1 || groupStore.joinRequests[0].RequestID != "gjr_request" ||
|
||||
groupStore.joinRequests[0].ApplicantUserID != target || groupStore.joinRequests[0].Source != "invitation" {
|
||||
t.Fatalf("join request was not created: %#v", groupStore.joinRequests)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/summary", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("summary status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
summary := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if summary["pending_join_request_count"] != float64(1) {
|
||||
t.Fatalf("unexpected summary response: %#v", summary)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/join-requests?status=pending", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("join requests status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
list := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
requests := list["items"].([]any)
|
||||
if len(requests) != 1 || requests[0].(map[string]any)["request_id"] != "gjr_request" {
|
||||
t.Fatalf("unexpected join requests response: %#v", list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupInvitationAccept(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user