fix: 优化群成员分页并补齐头像资料

This commit is contained in:
richjohnrambo
2026-09-09 09:38:20 +08:00
parent aae13c9265
commit 9ac2b412cc
7 changed files with 167 additions and 61 deletions
+9 -3
View File
@@ -3340,8 +3340,11 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt
// Fetch all subscribed users. The number of users is not large. // Fetch all subscribed users. The number of users is not large.
q := `SELECT s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid, q := `SELECT s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid,
s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private,
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature,
p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account
FROM im_subscriptions AS s JOIN im_users AS u ON s.userid=u.id FROM im_subscriptions AS s JOIN im_users AS u ON s.userid=u.id
LEFT JOIN im_user_profiles AS p ON p.user_id=s.userid
WHERE s.topic=?` WHERE s.topic=?`
args := []any{topic} args := []any{topic}
if !keepDeleted { if !keepDeleted {
@@ -3394,18 +3397,21 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt
var lastSeen sql.NullTime var lastSeen sql.NullTime
var userAgent string var userAgent string
var public, trusted any var public, trusted any
var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString
for rows.Next() { for rows.Next() {
if err = rows.Scan( if err = rows.Scan(
&sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt,
&sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId,
&sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven, &sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven,
&public, &trusted, &lastSeen, &userAgent, &sub.Private); err != nil { &public, &trusted, &lastSeen, &userAgent, &sub.Private,
&profileNickname, &profileAvatarFileID, &profileSignature,
&profileGender, &profileRegion, &profilePublicAccount); err != nil {
break break
} }
sub.User = common.EncodeUidString(sub.User).String() sub.User = common.EncodeUidString(sub.User).String()
sub.Private = common.FromJSON(sub.Private) sub.Private = common.FromJSON(sub.Private)
sub.SetPublic(common.FromJSON(public)) sub.SetPublic(profilePublicFromFields(common.FromJSON(public), profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount))
sub.SetTrusted(common.FromJSON(trusted)) sub.SetTrusted(common.FromJSON(trusted))
sub.SetLastSeenAndUA(&lastSeen.Time, userAgent) sub.SetLastSeenAndUA(&lastSeen.Time, userAgent)
subs = append(subs, sub) subs = append(subs, sub)
+9 -3
View File
@@ -1077,8 +1077,11 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b
} }
tcat := t.GetTopicCat(topic) tcat := t.GetTopicCat(topic)
q := `SELECT s.tenant_id,s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid, q := `SELECT s.tenant_id,s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid,
s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private,
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature,
p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account
FROM im_subscriptions AS s JOIN im_users AS u ON s.tenant_id=u.tenant_id AND s.userid=u.id FROM im_subscriptions AS s JOIN im_users AS u ON s.tenant_id=u.tenant_id AND s.userid=u.id
LEFT JOIN im_user_profiles AS p ON p.tenant_id=s.tenant_id AND p.user_id=s.userid
WHERE s.tenant_id=? AND s.topic=?` WHERE s.tenant_id=? AND s.topic=?`
args := []any{id, topic} args := []any{id, topic}
if !keepDeleted { if !keepDeleted {
@@ -1119,14 +1122,17 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b
var sub t.Subscription var sub t.Subscription
var lastSeen sql.NullTime var lastSeen sql.NullTime
var public, trusted any var public, trusted any
var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString
if err = rows.Scan(&sub.TenantID, &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, if err = rows.Scan(&sub.TenantID, &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt,
&sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven, &sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven,
&public, &trusted, &lastSeen, &userAgent, &sub.Private); err != nil { &public, &trusted, &lastSeen, &userAgent, &sub.Private,
&profileNickname, &profileAvatarFileID, &profileSignature,
&profileGender, &profileRegion, &profilePublicAccount); err != nil {
return nil, err return nil, err
} }
sub.User = common.EncodeUidString(sub.User).String() sub.User = common.EncodeUidString(sub.User).String()
sub.Private = common.FromJSON(sub.Private) sub.Private = common.FromJSON(sub.Private)
sub.SetPublic(common.FromJSON(public)) sub.SetPublic(profilePublicFromFields(common.FromJSON(public), profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount))
sub.SetTrusted(common.FromJSON(trusted)) sub.SetTrusted(common.FromJSON(trusted))
sub.SetLastSeenAndUA(&lastSeen.Time, userAgent) sub.SetLastSeenAndUA(&lastSeen.Time, userAgent)
subs = append(subs, sub) subs = append(subs, sub)
+44 -10
View File
@@ -248,30 +248,49 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou
if opts.Limit <= 0 || opts.Limit > 100 { if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 30 opts.Limit = 30
} }
where := []string{"topic=?", "member_status=?"} memberIndex := "idx_im_group_members_topic_status"
where := []string{"gm.topic=?", "gm.member_status=?"}
args := []any{topic, t.GroupMemberStatusActive} args := []any{topic, t.GroupMemberStatusActive}
if opts.Role != "" { if opts.Role != "" {
where = append(where, "role=?") memberIndex = "idx_im_group_members_topic_role"
where = append(where, "gm.role=?")
args = append(args, opts.Role) args = append(args, opts.Role)
} }
if opts.Query != "" { if opts.Query != "" {
where = append(where, "nickname LIKE ?") like := "%" + opts.Query + "%"
args = append(args, "%"+opts.Query+"%") where = append(where, `(gm.nickname LIKE ? OR p.nickname LIKE ? OR p.public_account LIKE ?
OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.fn')) LIKE ?
OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.name')) LIKE ?
OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.display_name')) LIKE ?
OR u.username LIKE ?)`)
args = append(args, like, like, like, like, like, like, like)
} }
if opts.Cursor != "" { if opts.Cursor != "" {
parsed, err := strconv.ParseInt(opts.Cursor, 10, 64) parsed, err := strconv.ParseInt(opts.Cursor, 10, 64)
if err != nil || parsed <= 0 { if err != nil || parsed < 0 {
return nil, "", t.ErrMalformed return nil, "", t.ErrMalformed
} }
where = append(where, "id>?") if parsed > 0 {
where = append(where, "gm.id>?")
args = append(args, parsed) args = append(args, parsed)
} }
}
if opts.Mentionable {
where = append(where, "INSTR(s.modewant,'J')>0 AND INSTR(s.modegiven,'J')>0")
}
args = append(args, opts.Limit+1) args = append(args, opts.Limit+1)
ctx, cancel := a.getContext() ctx, cancel := a.getContext()
if cancel != nil { if cancel != nil {
defer cancel() defer cancel()
} }
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_group_members WHERE "+strings.Join(where, " AND ")+" ORDER BY id LIMIT ?", args...) rows, err := a.db.QueryxContext(ctx, `SELECT STRAIGHT_JOIN gm.*,s.modewant,s.modegiven,u.public,u.trusted,
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature,
p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account
FROM im_group_members AS gm FORCE INDEX (`+memberIndex+`)
JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id AND s.deletedat IS NULL
JOIN im_users AS u ON u.id=gm.user_id AND u.state!=?
LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id
WHERE `+strings.Join(where, " AND ")+` ORDER BY gm.id LIMIT ?`, append([]any{t.StateDeleted}, args...)...)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@@ -279,15 +298,30 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou
items := make([]t.GroupMember, 0, opts.Limit) items := make([]t.GroupMember, 0, opts.Limit)
next := "" next := ""
for rows.Next() { for rows.Next() {
var item t.GroupMember row := struct {
if err = rows.StructScan(&item); err != nil { t.GroupMember
Public any
Trusted any
ProfileNickname sql.NullString `db:"profile_nickname"`
ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"`
ProfileSignature sql.NullString `db:"profile_signature"`
ProfileGender sql.NullString `db:"profile_gender"`
ProfileRegion sql.NullString `db:"profile_region"`
ProfilePublicAccount sql.NullString `db:"profile_public_account"`
}{}
if err = rows.StructScan(&row); err != nil {
return nil, "", err return nil, "", err
} }
item := row.GroupMember
scanGroupMember(&item) scanGroupMember(&item)
if len(items) == opts.Limit { if len(items) == opts.Limit {
next = strconv.FormatInt(item.ID, 10) if len(items) > 0 {
next = strconv.FormatInt(items[len(items)-1].ID, 10)
}
continue continue
} }
item.Public = profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount)
item.Trusted = common.FromJSON(row.Trusted)
items = append(items, item) items = append(items, item)
} }
return items, next, rows.Err() return items, next, rows.Err()
+53 -29
View File
@@ -4807,8 +4807,14 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
return return
} }
limit := parseIMLimit(req.URL.Query().Get("limit"), 30, 100) limit := parseIMLimit(req.URL.Query().Get("limit"), 30, 100)
cursor, err := parseIMNonNegativeCursor(req.URL.Query().Get("cursor")) cursor := strings.TrimSpace(req.URL.Query().Get("cursor"))
if err != nil { if cursor != "" {
if _, err := strconv.ParseInt(cursor, 10, 64); err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "cursor"})
return
}
}
if parsed, err := parseIMNonNegativeCursor(cursor); err != nil || parsed < 0 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "cursor"}) writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "cursor"})
return return
} }
@@ -4822,25 +4828,22 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
writeIMSuccess(wrt, req, cached) writeIMSuccess(wrt, req, cached)
return return
} }
members, err := store.Topics.GetUsers(principal.TenantID, topic, &types.QueryOpt{Limit: 1000}) members, nextCursor, err := store.GroupBusiness.ListMembers(principal.TenantID, topic, types.GroupMemberListOptions{
Role: roleFilter,
EligibleRole: eligibleRole,
Query: query,
Cursor: cursor,
Limit: limit,
Mentionable: mentionableOnly,
})
if err != nil { if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil) writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return return
} }
items := make([]map[string]any, 0, limit) items := make([]map[string]any, 0, limit)
seen := 0
nextCursor := ""
for _, member := range members { for _, member := range members {
access := member.ModeGiven & member.ModeWant access := groupMemberAccess(member)
mentionable := access.IsJoiner() && access.IsReader() item := groupBusinessMemberPayload(member)
role := groupMemberRole(access)
if roleFilter != "" && role != roleFilter {
continue
}
if mentionableOnly && !mentionable {
continue
}
item := groupMemberPayload(member, mentionable)
if eligibleRole != "" { if eligibleRole != "" {
eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole) eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole)
item["eligible"] = eligible item["eligible"] = eligible
@@ -4855,19 +4858,7 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
item["ineligible_reason"] = reason item["ineligible_reason"] = reason
} }
} }
if query != "" && !groupMemberMatches(item, query) {
continue
}
if seen < cursor {
seen++
continue
}
if len(items) == limit {
nextCursor = strconv.Itoa(seen)
break
}
items = append(items, item) items = append(items, item)
seen++
} }
permissions := map[string]any{ permissions := map[string]any{
"can_mention_all": selfAccess.IsAdmin(), "can_mention_all": selfAccess.IsAdmin(),
@@ -7320,6 +7311,38 @@ func groupMemberPayload(sub types.Subscription, mentionable bool) map[string]any
return data return data
} }
func groupBusinessMemberPayload(member types.GroupMember) map[string]any {
access := groupMemberAccess(member)
user := &types.User{ObjHeader: types.ObjHeader{Id: member.UserID.String()}, Public: member.Public, Trusted: member.Trusted}
data := userSummaryPayload(user, "")
if nickname := strings.TrimSpace(member.Nickname); nickname != "" {
data["group_nickname"] = nickname
}
data["role"] = groupMemberRole(access)
data["mentionable"] = access.IsJoiner() && access.IsReader()
data["acs"] = map[string]any{
"mode": access.String(),
"want": member.ModeWant.String(),
"given": member.ModeGiven.String(),
}
return data
}
func groupMemberAccess(member types.GroupMember) types.AccessMode {
access := member.ModeGiven & member.ModeWant
if access != types.ModeNone {
return access
}
switch member.Role {
case types.GroupMemberRoleOwner:
return types.ModeCFull
case types.GroupMemberRoleAdmin:
return types.ModeCPublic | types.ModeApprove
default:
return types.ModeCPublic
}
}
func groupMemberProfilePayload(topic string, sub *types.Subscription, user *types.User, ctx *types.RelationshipContext) map[string]any { func groupMemberProfilePayload(topic string, sub *types.Subscription, user *types.User, ctx *types.RelationshipContext) map[string]any {
data := groupMemberPayload(*sub, true) data := groupMemberPayload(*sub, true)
data["topic"] = topic data["topic"] = topic
@@ -7771,16 +7794,17 @@ func groupMemberMatches(item map[string]any, query string) bool {
} }
func groupMemberPageCacheKey(topic string, uid types.Uid, access types.AccessMode, query, roleFilter, eligibleRole string, func groupMemberPageCacheKey(topic string, uid types.Uid, access types.AccessMode, query, roleFilter, eligibleRole string,
limit, cursor int, mentionableOnly, muteEligible bool) string { limit int, cursor string, mentionableOnly, muteEligible bool) string {
return strings.Join([]string{ return strings.Join([]string{
topic, topic,
"v3",
uid.UserId(), uid.UserId(),
access.String(), access.String(),
strings.ToLower(strings.TrimSpace(query)), strings.ToLower(strings.TrimSpace(query)),
strings.TrimSpace(roleFilter), strings.TrimSpace(roleFilter),
strings.TrimSpace(eligibleRole), strings.TrimSpace(eligibleRole),
strconv.Itoa(limit), strconv.Itoa(limit),
strconv.Itoa(cursor), strings.TrimSpace(cursor),
strconv.FormatBool(mentionableOnly), strconv.FormatBool(mentionableOnly),
strconv.FormatBool(muteEligible), strconv.FormatBool(muteEligible),
}, "|") }, "|")
+31 -13
View File
@@ -345,6 +345,9 @@ type testGroupBusinessStore struct {
mutes []types.GroupMute mutes []types.GroupMute
files []types.GroupFile files []types.GroupFile
invitations []types.GroupInvitation invitations []types.GroupInvitation
listMembers []types.GroupMember
listMembersOpts types.GroupMemberListOptions
listMembersNext string
updatedSettings types.GroupSettingsPatch updatedSettings types.GroupSettingsPatch
updatedMembers []types.GroupMemberPatch updatedMembers []types.GroupMemberPatch
updatedRoleMode types.AccessMode updatedRoleMode types.AccessMode
@@ -402,8 +405,9 @@ func (s *testGroupBusinessStore) GetMember(_ types.TenantID, topic string, uid t
return &types.GroupMember{Topic: topic, UserID: uid, Role: types.GroupMemberRoleMember, MemberStatus: types.GroupMemberStatusActive, Version: 1}, nil return &types.GroupMember{Topic: topic, UserID: uid, Role: types.GroupMemberRoleMember, MemberStatus: types.GroupMemberStatusActive, Version: 1}, nil
} }
func (s *testGroupBusinessStore) ListMembers(_ types.TenantID, _ string, _ types.GroupMemberListOptions) ([]types.GroupMember, string, error) { func (s *testGroupBusinessStore) ListMembers(_ types.TenantID, _ string, opts types.GroupMemberListOptions) ([]types.GroupMember, string, error) {
return nil, "", nil s.listMembersOpts = opts
return s.listMembers, s.listMembersNext, nil
} }
func (s *testGroupBusinessStore) UpdateMember(_ types.TenantID, topic string, uid types.Uid, patch types.GroupMemberPatch) (*types.GroupMember, error) { func (s *testGroupBusinessStore) UpdateMember(_ types.TenantID, topic string, uid types.Uid, patch types.GroupMemberPatch) (*types.GroupMember, error) {
@@ -3877,14 +3881,14 @@ func TestServeIMGroupMembers(t *testing.T) {
defer ctrl.Finish() defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42) expectIMBearer(t, ctrl, "token-1", 7, 42)
oldSubs := store.Subs oldSubs := store.Subs
oldTopics := store.Topics oldGroupBusiness := store.GroupBusiness
subs := mock_store.NewMockSubsPersistenceInterface(ctrl) subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
topics := mock_store.NewMockTopicsPersistenceInterface(ctrl)
store.Subs = subs store.Subs = subs
store.Topics = topics groupStore := &testGroupBusinessStore{listMembersNext: "101"}
store.GroupBusiness = groupStore
defer func() { defer func() {
store.Subs = oldSubs store.Subs = oldSubs
store.Topics = oldTopics store.GroupBusiness = oldGroupBusiness
}() }()
subs.EXPECT().Get(types.TenantID(7), "grpMention", types.Uid(42), false).Return(&types.Subscription{ subs.EXPECT().Get(types.TenantID(7), "grpMention", types.Uid(42), false).Return(&types.Subscription{
@@ -3894,13 +3898,18 @@ func TestServeIMGroupMembers(t *testing.T) {
ModeWant: types.ModeCFull, ModeWant: types.ModeCFull,
ModeGiven: types.ModeCFull, ModeGiven: types.ModeCFull,
}, nil) }, nil)
alice := types.Subscription{User: types.Uid(101).UserId(), ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic} groupStore.listMembers = []types.GroupMember{{
alice.SetPublic(map[string]any{"display_name": "Alice"}) ID: 101,
bob := types.Subscription{User: types.Uid(102).UserId(), ModeWant: types.ModeJoin, ModeGiven: types.ModeJoin} Topic: "grpMention",
bob.SetPublic(map[string]any{"display_name": "Bob"}) UserID: types.Uid(101),
topics.EXPECT().GetUsers(types.TenantID(7), "grpMention", gomock.Any()).Return([]types.Subscription{alice, bob}, nil) Role: types.GroupMemberRoleMember,
ModeWant: types.ModeCPublic,
ModeGiven: types.ModeCPublic,
Public: map[string]any{"display_name": "Alice", "avatar_file_id": "alice.jpg"},
MemberStatus: types.GroupMemberStatusActive,
}}
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpMention/members?query=ali&mentionable=true", nil) req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpMention/members?query=ali&mentionable=true&limit=20&cursor=99", nil)
req.Header.Set("Authorization", "Bearer token-1") req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -3911,12 +3920,21 @@ func TestServeIMGroupMembers(t *testing.T) {
} }
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
items := data["items"].([]any) items := data["items"].([]any)
if len(items) != 1 || items[0].(map[string]any)["display_name"] != "Alice" || items[0].(map[string]any)["mentionable"] != true { if len(items) != 1 || items[0].(map[string]any)["display_name"] != "Alice" ||
items[0].(map[string]any)["avatar_file_id"] != "alice.jpg" ||
items[0].(map[string]any)["mentionable"] != true {
t.Fatalf("unexpected members response: %#v", data) t.Fatalf("unexpected members response: %#v", data)
} }
if data["next_cursor"] != "101" {
t.Fatalf("unexpected cursor: %#v", data)
}
if data["permissions"].(map[string]any)["can_mention_all"] != true { if data["permissions"].(map[string]any)["can_mention_all"] != true {
t.Fatalf("expected can_mention_all permission: %#v", data) t.Fatalf("expected can_mention_all permission: %#v", data)
} }
if groupStore.listMembersOpts.Query != "ali" || !groupStore.listMembersOpts.Mentionable ||
groupStore.listMembersOpts.Limit != 20 || groupStore.listMembersOpts.Cursor != "99" {
t.Fatalf("members options were not forwarded: %#v", groupStore.listMembersOpts)
}
} }
func TestGroupMemberPayloadUserIDFormats(t *testing.T) { func TestGroupMemberPayloadUserIDFormats(t *testing.T) {
+15 -2
View File
@@ -2904,7 +2904,7 @@ func refreshUserProfileProjections(tenantID types.TenantID, uid types.Uid, profi
items, nextCursor, err := Relations.ListFriends(tenantID, uid, "", "", cursor, 100) items, nextCursor, err := Relations.ListFriends(tenantID, uid, "", "", cursor, 100)
if err != nil { if err != nil {
logs.Warn.Printf("profile %s: refresh friend subscription projections failed: %v", uid, err) logs.Warn.Printf("profile %s: refresh friend subscription projections failed: %v", uid, err)
return break
} }
for _, item := range items { for _, item := range items {
if item.Friendship.Topic == "" || item.Friendship.PeerID.IsZero() { if item.Friendship.Topic == "" || item.Friendship.PeerID.IsZero() {
@@ -2915,10 +2915,23 @@ func refreshUserProfileProjections(tenantID types.TenantID, uid types.Uid, profi
} }
} }
if nextCursor == "" { if nextCursor == "" {
return break
} }
cursor = nextCursor cursor = nextCursor
} }
if topics, err := Users.GetTopics(tenantID, uid, &types.QueryOpt{Limit: 1000}); err != nil {
logs.Warn.Printf("profile %s: refresh group member projections failed: %v", uid, err)
} else {
for _, sub := range topics {
if types.GetTopicCat(sub.Topic) != types.TopicCatGrp {
continue
}
if err := Subs.Update(tenantID, sub.Topic, uid, map[string]any{"Public": public}); err != nil {
logs.Warn.Printf("profile %s: refresh group subscription %s public failed: %v", uid, sub.Topic, err)
}
invalidateGroupMemberPages(tenantID, sub.Topic)
}
}
} }
func userProfilePublicProjection(profile *types.UserProfile) map[string]any { func userProfilePublicProjection(profile *types.UserProfile) map[string]any {
+5
View File
@@ -73,6 +73,10 @@ type GroupMember struct {
Role string `db:"role"` Role string `db:"role"`
MemberStatus string `db:"member_status"` MemberStatus string `db:"member_status"`
Nickname string `db:"nickname"` Nickname string `db:"nickname"`
ModeWant AccessMode `db:"modewant"`
ModeGiven AccessMode `db:"modegiven"`
Public any `db:"public"`
Trusted any `db:"trusted"`
Pinned bool `db:"pinned"` Pinned bool `db:"pinned"`
MuteNotifications bool `db:"mute_notifications"` MuteNotifications bool `db:"mute_notifications"`
JoinSource string `db:"join_source"` JoinSource string `db:"join_source"`
@@ -105,6 +109,7 @@ type GroupMemberListOptions struct {
Query string Query string
Cursor string Cursor string
Limit int Limit int
Mentionable bool
} }
type GroupInvitation struct { type GroupInvitation struct {