diff --git a/server/im_http.go b/server/im_http.go index f2aa80c..a38360a 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -5619,6 +5619,11 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s } if joined { item["status"] = types.GroupInvitationStatusJoined + if imReliableHTTPEventsEnabled(req) { + publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, uid, + "有成员通过邀请加入了群聊。", + map[string]any{"source": "direct_invitation"}) + } } else { item["status"] = "already_member" } @@ -5863,6 +5868,11 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic } publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined", map[string]any{"user_id": principal.Uid.UserId(), "joined": joined, "source": "open_group"}) + if joined && imReliableHTTPEventsEnabled(req) { + publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, principal.Uid, + "有成员加入了群聊。", + map[string]any{"source": "open_group"}) + } writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "joined", "joined": joined, "source": "open_group"}) return } @@ -11652,6 +11662,28 @@ func publishIMGroupInvitationNotification(tenantID types.TenantID, inv *types.Gr } } +func publishIMGroupMemberJoinedSystemNotice(tenantID types.TenantID, topic string, inviter, target types.Uid, text string, extra map[string]any) { + if target.IsZero() { + return + } + actors := make([]imSystemMessageActor, 0, 2) + if !inviter.IsZero() && inviter != target { + actors = append(actors, imSystemMessageActor{ + UserID: inviter.UserId(), + Click: map[string]any{"type": "user_profile", "target_id": inviter.UserId()}, + }) + } + actors = append(actors, imSystemMessageActor{ + UserID: target.UserId(), + Click: map[string]any{"type": "user_profile", "target_id": target.UserId()}, + }) + publishIMConversationSystemNotice(tenantID, topic, + "system_action", "group_member_joined", text, + actors, + []imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}}, + extra) +} + func deliverIMConversationSystemMessageToP2PSessions(tenantID types.TenantID, msg *types.Message, out *ServerComMessage) { if globals.sessionStore == nil || msg == nil || out == nil || out.Data == nil { return diff --git a/server/im_http_test.go b/server/im_http_test.go index 76c2f24..2ef0703 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -5379,6 +5379,74 @@ func TestServeIMGroupQRCodesAndResolve(t *testing.T) { } } +func TestServeIMGroupInvitationDirectJoinWritesSystemNotice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42) + mockStore.EXPECT().IsOpen().Return(true).AnyTimes() + + oldSubs := store.Subs + oldTopics := store.Topics + oldMessages := store.Messages + oldProfiles := store.Profiles + oldGroupBusiness := store.GroupBusiness + subs := mock_store.NewMockSubsPersistenceInterface(ctrl) + topics := mock_store.NewMockTopicsPersistenceInterface(ctrl) + messages := mock_store.NewMockMessagesPersistenceInterface(ctrl) + store.Subs = subs + store.Topics = topics + store.Messages = messages + store.Profiles = nil + store.GroupBusiness = &testGroupBusinessStore{} + t.Cleanup(func() { + store.Subs = oldSubs + store.Topics = oldTopics + store.Messages = oldMessages + store.Profiles = oldProfiles + store.GroupBusiness = oldGroupBusiness + }) + + subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(&types.Subscription{ + TenantID: 7, User: types.Uid(42).String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull, + }, nil) + subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(43), false).Return(nil, types.ErrNotFound) + topics.EXPECT().Get(types.TenantID(7), "grpAlpha").Return(&types.Topic{TenantID: 7, SeqId: 7}, nil) + messages.EXPECT().Save(types.TenantID(7), gomock.Any(), nil, false).DoAndReturn( + func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) { + if msg.Topic != "grpAlpha" || msg.SeqId != 8 { + t.Fatalf("unexpected system notice message: %#v", msg) + } + if msg.Head["x-im-template"] != "group_member_joined" || msg.Head["x-im-type"] != "system_action" { + t.Fatalf("unexpected system notice head: %#v", msg.Head) + } + content := msg.Content.(map[string]any) + if content["text"] != "有成员通过邀请加入了群聊。" || content["template"] != "group_member_joined" { + t.Fatalf("unexpected system notice content: %#v", content) + } + extra := content["extra"].(map[string]any) + if extra["source"] != "direct_invitation" { + t.Fatalf("unexpected system notice extra: %#v", extra) + } + return &types.MessageSaveResult{SeqId: msg.SeqId}, nil + }) + + body := fmt.Sprintf(`{"user_ids":["%s"],"message":"欢迎"}`, types.Uid(43).UserId()) + req := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/invitations", strings.NewReader(body)) + req.Header.Set("Authorization", "Bearer token-1") + req.Header.Set("Idempotency-Key", "invite-1") + req = req.WithContext(context.WithValue(req.Context(), imHTTPProtocolValidated, true)) + rec := httptest.NewRecorder() + serveIMGroupResource(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("invite status: got %d body=%s", rec.Code, rec.Body.String()) + } + data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) + items := data["items"].([]any) + if len(items) != 1 || items[0].(map[string]any)["status"] != types.GroupInvitationStatusJoined { + t.Fatalf("unexpected invitation response: %#v", data) + } +} + func TestServeIMGroupAnnouncementGovernanceJoinRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()