Files
im_server/server/im_group_events.go
T

189 lines
5.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
func publishIMGroupRealtimeEvent(tenantID types.TenantID, topic string, actor types.Uid, eventType string, payload any) {
topic = strings.TrimSpace(topic)
eventType = strings.TrimSpace(eventType)
if tenantID.IsZero() || topic == "" || eventType == "" {
return
}
payload = enrichIMGroupMemberEventPayload(eventType, payload)
encoded, err := json.Marshal(payload)
if err != nil {
logs.Warn.Printf("group realtime: encode event topic=%s event=%s: %v", topic, eventType, err)
encoded = []byte(`{}`)
}
from := ""
if !actor.IsZero() {
from = actor.UserId()
}
if isAdminOnlyIMGroupEvent(eventType) {
publishIMGroupEventToAdmins(tenantID, topic, actor, eventType, encoded, payload)
return
}
message := &ServerComMessage{
TenantID: tenantID,
RcptTo: topic,
AsUser: from,
Timestamp: time.Now().UTC().Round(time.Millisecond),
Info: &MsgServerInfo{
Topic: topic,
From: from,
What: "group",
Event: eventType,
Payload: json.RawMessage(encoded),
},
}
if globals.hub != nil {
select {
case globals.hub.routeSrv <- message:
default:
logs.Warn.Printf("group realtime: hub queue full topic=%s event=%s", topic, eventType)
}
}
publishRealtimeTopicMessage(topic, message)
}
func isAdminOnlyIMGroupEvent(eventType string) bool {
switch eventType {
case "group.member.updated", "group.member.left", "group.role.updated", "group.mute.updated",
"group.invitations.updated", "group.join_request.created", "group.join_request.accepted",
"group.join_request.rejected":
return true
default:
return false
}
}
func publishIMGroupEventToAdmins(tenantID types.TenantID, topic string, actor types.Uid, eventType string,
encoded json.RawMessage, payload any) {
recipients := map[types.Uid]struct{}{}
if !actor.IsZero() {
recipients[actor] = struct{}{}
}
collectIMGroupEventUserIDs(payload, recipients)
for _, role := range []string{types.GroupMemberRoleOwner, types.GroupMemberRoleAdmin} {
if store.GroupBusiness == nil {
break
}
cursor := ""
for {
members, next, err := store.GroupBusiness.ListMembers(tenantID, topic, types.GroupMemberListOptions{
Role: role, Cursor: cursor, Limit: 100,
})
if err != nil {
logs.Warn.Printf("group realtime: list %s recipients topic=%s event=%s: %v", role, topic, eventType, err)
break
}
for _, member := range members {
if !member.UserID.IsZero() {
recipients[member.UserID] = struct{}{}
}
}
if next == "" || next == cursor {
break
}
cursor = next
}
}
for uid := range recipients {
message := &ServerComMessage{
TenantID: tenantID, RcptTo: uid.UserId(), AsUser: actor.UserId(),
Timestamp: time.Now().UTC().Round(time.Millisecond),
Info: &MsgServerInfo{Topic: topic, From: actor.UserId(), What: "group", Event: eventType, Payload: encoded},
}
if globals.sessionStore != nil {
globals.sessionStore.DeliverToUser(tenantID, uid, "", message)
}
publishRealtimeUserMessage(uid, message)
}
}
func collectIMGroupEventUserIDs(value any, out map[types.Uid]struct{}) {
switch typed := value.(type) {
case map[string]any:
if ok, exists := typed["ok"].(bool); exists && !ok {
return
}
for key, item := range typed {
switch key {
case "user_id", "target_id", "target_user_id", "applicant_id", "applicant_user_id", "owner_id":
if uid := imGroupEventUID(item); !uid.IsZero() {
out[uid] = struct{}{}
}
}
collectIMGroupEventUserIDs(item, out)
}
case []map[string]any:
for _, item := range typed {
collectIMGroupEventUserIDs(item, out)
}
case []any:
for _, item := range typed {
collectIMGroupEventUserIDs(item, out)
}
}
}
func imGroupEventUID(value any) types.Uid {
if uid := types.ParseUserId(strings.TrimSpace(fmt.Sprint(value))); !uid.IsZero() {
return uid
}
switch typed := value.(type) {
case int64:
return store.EncodeUid(typed)
case int:
return store.EncodeUid(int64(typed))
case float64:
return store.EncodeUid(int64(typed))
}
return types.ZeroUid
}
func enrichIMGroupMemberEventPayload(eventType string, payload any) any {
data, ok := payload.(map[string]any)
if !ok {
return payload
}
copyPayload := make(map[string]any, len(data)+2)
for key, value := range data {
copyPayload[key] = value
}
switch eventType {
case "group.member.left":
copyPayload["member_sync"] = "remove"
case "group.member.joined", "group.member.updated", "group.member.profile.updated", "group.role.updated", "group.mute.updated":
if _, ok := copyPayload["user_id"]; ok {
copyPayload["member_sync"] = "single"
} else {
copyPayload["member_sync"] = "full"
}
}
if isAdminOnlyIMGroupEvent(eventType) {
copyPayload["visibility"] = "admins_and_affected"
} else if eventType == "group.member.joined" || eventType == "group.member.profile.updated" {
copyPayload["visibility"] = "all_members"
}
return copyPayload
}
func userIDs(uids []types.Uid) []string {
ids := make([]string, 0, len(uids))
for _, uid := range uids {
if !uid.IsZero() {
ids = append(ids, uid.UserId())
}
}
return ids
}