Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -3161,6 +3161,50 @@ func serveIMMessageReceipts(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// broadcastIMMessageRecalled pushes the native {pres what:"del" delseq} frame to
|
||||||
|
// the sessions currently attached to the topic so peers learn about a recall in
|
||||||
|
// real time (the REST response alone reaches only the caller). The wire record
|
||||||
|
// carries "low" only (no "hi"): client SDKs then flush the message from their
|
||||||
|
// local cache and leave a content-less tombstone behind, which the webapp
|
||||||
|
// renders as the usual "recalled" placeholder. Routed through the hub so the
|
||||||
|
// p2p topic rewrites the frame's topic name to each recipient's own view.
|
||||||
|
func broadcastIMMessageRecalled(tenantID types.TenantID, topic string, seqs []int64, actor types.Uid) {
|
||||||
|
if globals.hub == nil || topic == "" || actor.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delseq := make([]MsgRange, 0, len(seqs))
|
||||||
|
for _, seq := range seqs {
|
||||||
|
if seq > 0 {
|
||||||
|
delseq = append(delseq, MsgRange{LowId: seq})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(delseq) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// P2P topics are loaded under their canonical name with xoriginal cleared,
|
||||||
|
// so the presence frame must carry an empty topic name to pass the
|
||||||
|
// handlePresence xoriginal check; any other category uses the topic name.
|
||||||
|
presTopic := topic
|
||||||
|
if types.GetTopicCat(topic) == types.TopicCatP2P {
|
||||||
|
presTopic = ""
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case globals.hub.routeSrv <- &ServerComMessage{
|
||||||
|
TenantID: tenantID,
|
||||||
|
RcptTo: topic,
|
||||||
|
Pres: &MsgServerPres{
|
||||||
|
Topic: presTopic,
|
||||||
|
What: "del",
|
||||||
|
Src: actor.UserId(),
|
||||||
|
DelSeq: delseq,
|
||||||
|
FilterIn: int(types.ModeRead),
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
logs.Warn.Printf("im message recall: hub presence queue full topic=%s count=%d", topic, len(delseq))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -3215,6 +3259,7 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
broadcastIMMessageRecalled(principal.TenantID, updated.Topic, []int64{seq}, principal.Uid)
|
||||||
writeIMSuccess(wrt, req, messageMutationPayload(updated, "recalled"))
|
writeIMSuccess(wrt, req, messageMutationPayload(updated, "recalled"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3242,6 +3287,7 @@ func serveIMMessageBatchRecall(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, len(body.Items))
|
items := make([]map[string]any, 0, len(body.Items))
|
||||||
|
recalledSeqs := make([]int64, 0, len(body.Items))
|
||||||
recalledAt := time.Now().UTC().Round(time.Millisecond)
|
recalledAt := time.Now().UTC().Round(time.Millisecond)
|
||||||
for _, item := range body.Items {
|
for _, item := range body.Items {
|
||||||
if item.Seq <= 0 {
|
if item.Seq <= 0 {
|
||||||
@@ -3281,7 +3327,9 @@ func serveIMMessageBatchRecall(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = append(items, result)
|
items = append(items, result)
|
||||||
|
recalledSeqs = append(recalledSeqs, item.Seq)
|
||||||
}
|
}
|
||||||
|
broadcastIMMessageRecalled(principal.TenantID, topic, recalledSeqs, principal.Uid)
|
||||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": items})
|
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4580,6 +4628,8 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
|||||||
}
|
}
|
||||||
if joined {
|
if joined {
|
||||||
item["status"] = types.GroupInvitationStatusJoined
|
item["status"] = types.GroupInvitationStatusJoined
|
||||||
|
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined",
|
||||||
|
map[string]any{"user_id": uid.UserId(), "joined": true, "source": "direct_invitation"})
|
||||||
if imReliableHTTPEventsEnabled(req) {
|
if imReliableHTTPEventsEnabled(req) {
|
||||||
publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, uid,
|
publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, uid,
|
||||||
"有成员通过邀请加入了群聊。",
|
"有成员通过邀请加入了群聊。",
|
||||||
@@ -4941,6 +4991,10 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
|
|||||||
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
|
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
|
||||||
map[string]any{"request_id": updated.RequestID, "source": "approval"})
|
map[string]any{"request_id": updated.RequestID, "source": "approval"})
|
||||||
}
|
}
|
||||||
|
if updated.Status == types.GroupJoinRequestStatusAccepted {
|
||||||
|
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined",
|
||||||
|
map[string]any{"request_id": updated.RequestID, "user_id": updated.ApplicantUserID.UserId(), "joined": true, "source": "approval"})
|
||||||
|
}
|
||||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request."+updated.Status,
|
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request."+updated.Status,
|
||||||
map[string]any{"request_id": updated.RequestID, "user_id": updated.ApplicantUserID.UserId(), "status": updated.Status})
|
map[string]any{"request_id": updated.RequestID, "user_id": updated.ApplicantUserID.UserId(), "status": updated.Status})
|
||||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
||||||
|
|||||||
@@ -3715,16 +3715,19 @@ func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||||
|
oldHub := globals.hub
|
||||||
oldSubs := store.Subs
|
oldSubs := store.Subs
|
||||||
oldGroupBusiness := store.GroupBusiness
|
oldGroupBusiness := store.GroupBusiness
|
||||||
oldSecurity := store.Security
|
oldSecurity := store.Security
|
||||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||||
groupStore := &testGroupBusinessStore{}
|
groupStore := &testGroupBusinessStore{}
|
||||||
securityStore := &testUserSecurityStore{}
|
securityStore := &testUserSecurityStore{}
|
||||||
|
globals.hub = &Hub{routeSrv: make(chan *ServerComMessage, 2)}
|
||||||
store.Subs = subs
|
store.Subs = subs
|
||||||
store.GroupBusiness = groupStore
|
store.GroupBusiness = groupStore
|
||||||
store.Security = securityStore
|
store.Security = securityStore
|
||||||
defer func() {
|
defer func() {
|
||||||
|
globals.hub = oldHub
|
||||||
store.Subs = oldSubs
|
store.Subs = oldSubs
|
||||||
store.GroupBusiness = oldGroupBusiness
|
store.GroupBusiness = oldGroupBusiness
|
||||||
store.Security = oldSecurity
|
store.Security = oldSecurity
|
||||||
@@ -3758,6 +3761,21 @@ func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
|||||||
if len(securityStore.notifications) != 0 {
|
if len(securityStore.notifications) != 0 {
|
||||||
t.Fatalf("direct invite should not create approval notification: %#v", securityStore.notifications)
|
t.Fatalf("direct invite should not create approval notification: %#v", securityStore.notifications)
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case msg := <-globals.hub.routeSrv:
|
||||||
|
if msg.Info == nil || msg.Info.Event != "group.member.joined" || msg.Info.Topic != "grpAlpha" {
|
||||||
|
t.Fatalf("unexpected realtime event: %#v", msg)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(msg.Info.Payload, &payload); err != nil {
|
||||||
|
t.Fatalf("decode payload: %v", err)
|
||||||
|
}
|
||||||
|
if payload["user_id"] != target.UserId() || payload["member_sync"] != "single" || payload["source"] != "direct_invitation" {
|
||||||
|
t.Fatalf("unexpected member joined payload: %#v", payload)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("missing group.member.joined realtime event")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) {
|
func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) {
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ func (t *Topic) presUsersOfInterest(what, ua string) {
|
|||||||
|
|
||||||
// Publish user's update to his/her users of interest on their 'me' topic while user's 'me' topic is offline
|
// Publish user's update to his/her users of interest on their 'me' topic while user's 'me' topic is offline
|
||||||
// Case A: user is being deleted, "gone".
|
// Case A: user is being deleted, "gone".
|
||||||
|
// Case B: user's last session just disconnected, "off".
|
||||||
func presUsersOfInterestOffline(tenantID types.TenantID, uid types.Uid, subs []types.Subscription, what string) {
|
func presUsersOfInterestOffline(tenantID types.TenantID, uid types.Uid, subs []types.Subscription, what string) {
|
||||||
// Push update to subscriptions
|
// Push update to subscriptions
|
||||||
for i := range subs {
|
for i := range subs {
|
||||||
@@ -286,6 +287,37 @@ func presUsersOfInterestOffline(tenantID types.TenantID, uid types.Uid, subs []t
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if types.GetTopicCat(subs[i].Topic) == types.TopicCatP2P {
|
||||||
|
// A presence request routed to the p2p topic would be silently
|
||||||
|
// dropped: the topic is loaded under its canonical p2p name while
|
||||||
|
// the request carries Topic="me" (notifyOnOrSkip), so the
|
||||||
|
// xoriginal check in handlePresence rejects it. Deliver to the
|
||||||
|
// contact's 'me' topic directly instead, the same way
|
||||||
|
// presSingleUserOffline does.
|
||||||
|
uid1, uid2, err := types.ParseP2P(subs[i].Topic)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contact := uid1
|
||||||
|
if contact == uid {
|
||||||
|
contact = uid2
|
||||||
|
}
|
||||||
|
if contact == uid || contact.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routeUserServerMessage(contact, &ServerComMessage{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Pres: &MsgServerPres{
|
||||||
|
Topic: notifyOn,
|
||||||
|
What: what,
|
||||||
|
Src: subs[i].Topic,
|
||||||
|
WantReply: false,
|
||||||
|
},
|
||||||
|
RcptTo: contact.UserId(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
globals.hub.routeSrv <- &ServerComMessage{
|
globals.hub.routeSrv <- &ServerComMessage{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
Pres: &MsgServerPres{
|
Pres: &MsgServerPres{
|
||||||
|
|||||||
+11
-1
@@ -465,9 +465,19 @@ func (s *Session) cleanUp(expired bool) {
|
|||||||
if err := store.SecurityAudit.CloseLogin(s.tenantID, s.sid, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
if err := store.SecurityAudit.CloseLogin(s.tenantID, s.sid, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||||
logs.Warn.Println("s.cleanUp: failed to close login audit", s.uid, err, s.sid)
|
logs.Warn.Println("s.cleanUp: failed to close login audit", s.uid, err, s.sid)
|
||||||
}
|
}
|
||||||
if err := store.MarkUserOffline(s.tenantID, s.uid, s.sid, currentNodeID()); err != nil {
|
remaining, err := store.MarkUserOffline(s.tenantID, s.uid, s.sid, currentNodeID())
|
||||||
|
if err != nil {
|
||||||
logs.Warn.Println("s.cleanUp: failed to mark user offline", s.uid, err, s.sid)
|
logs.Warn.Println("s.cleanUp: failed to mark user offline", s.uid, err, s.sid)
|
||||||
}
|
}
|
||||||
|
// 最后一个会话已断开(如主动登出、断网):立即通知关注者该用户离线,
|
||||||
|
// 不再等待 me topic 闲置超时后的 presUsersOfInterest("off")。
|
||||||
|
if err == nil && remaining == 0 && globals.hub != nil {
|
||||||
|
if uoi, uerr := store.Users.GetSubs(s.tenantID, s.uid); uerr == nil {
|
||||||
|
presUsersOfInterestOffline(s.tenantID, s.uid, uoi, "off")
|
||||||
|
} else {
|
||||||
|
logs.Warn.Println("s.cleanUp: failed to notify users of interest", s.uid, uerr, s.sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.purgeChannels()
|
s.purgeChannels()
|
||||||
s.inflightReqs.Wait()
|
s.inflightReqs.Wait()
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ type groupMessageRuntime interface {
|
|||||||
InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error
|
InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error
|
||||||
MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error
|
MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error
|
||||||
RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
|
RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
|
||||||
MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
|
MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) (int, error)
|
||||||
GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error)
|
GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error)
|
||||||
MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
||||||
MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
||||||
@@ -300,9 +300,9 @@ func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID str
|
|||||||
return groupMsgRuntime.RefreshUserPresence(tenantID, uid, sid, nodeID)
|
return groupMsgRuntime.RefreshUserPresence(tenantID, uid, sid, nodeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
func MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) (int, error) {
|
||||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||||
return nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
return groupMsgRuntime.MarkUserOffline(tenantID, uid, sid, nodeID)
|
return groupMsgRuntime.MarkUserOffline(tenantID, uid, sid, nodeID)
|
||||||
}
|
}
|
||||||
@@ -939,7 +939,7 @@ func (r *redisGroupMessageRuntime) RefreshUserPresence(tenantID types.TenantID,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) (int, error) {
|
||||||
if nodeID == "" {
|
if nodeID == "" {
|
||||||
nodeID = "local"
|
nodeID = "local"
|
||||||
}
|
}
|
||||||
@@ -956,11 +956,11 @@ func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid
|
|||||||
pipe.SRem(ctx, sessionsKey, member)
|
pipe.SRem(ctx, sessionsKey, member)
|
||||||
pipe.SRem(ctx, nodeKey, uid.UserId())
|
pipe.SRem(ctx, nodeKey, uid.UserId())
|
||||||
if _, err := pipe.Exec(ctx); err != nil {
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
count, err := r.client.SCard(ctx, sessionsKey).Result()
|
count, err := r.client.SCard(ctx, sessionsKey).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
pipe = r.client.Pipeline()
|
pipe = r.client.Pipeline()
|
||||||
@@ -968,14 +968,14 @@ func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid
|
|||||||
pipe.Expire(ctx, userKey, ttl)
|
pipe.Expire(ctx, userKey, ttl)
|
||||||
pipe.Expire(ctx, sessionsKey, ttl)
|
pipe.Expire(ctx, sessionsKey, ttl)
|
||||||
_, err = pipe.Exec(ctx)
|
_, err = pipe.Exec(ctx)
|
||||||
return err
|
return int(count), err
|
||||||
}
|
}
|
||||||
pipe = r.client.Pipeline()
|
pipe = r.client.Pipeline()
|
||||||
pipe.HSet(ctx, userKey, "online", "0", "session_count", 0, "node_id", nodeID, "last_seen_at", now.Format(time.RFC3339Nano))
|
pipe.HSet(ctx, userKey, "online", "0", "session_count", 0, "node_id", nodeID, "last_seen_at", now.Format(time.RFC3339Nano))
|
||||||
pipe.Expire(ctx, userKey, ttl)
|
pipe.Expire(ctx, userKey, ttl)
|
||||||
pipe.Del(ctx, sessionsKey)
|
pipe.Del(ctx, sessionsKey)
|
||||||
_, err = pipe.Exec(ctx)
|
_, err = pipe.Exec(ctx)
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *redisGroupMessageRuntime) GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
|
func (r *redisGroupMessageRuntime) GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ func (r *testGroupMessageRuntime) RefreshUserPresence(types.TenantID, types.Uid,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *testGroupMessageRuntime) MarkUserOffline(types.TenantID, types.Uid, string, string) error {
|
func (r *testGroupMessageRuntime) MarkUserOffline(types.TenantID, types.Uid, string, string) (int, error) {
|
||||||
return nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *testGroupMessageRuntime) GetUserPresence(types.TenantID, types.Uid) (*UserPresence, error) {
|
func (r *testGroupMessageRuntime) GetUserPresence(types.TenantID, types.Uid) (*UserPresence, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user