fix room rocket test im delivery
This commit is contained in:
parent
40157ef64b
commit
1224f3fbc6
@ -86,6 +86,29 @@ func RegionBroadcastGroupIDWithPrefix(prefix string, value string, regionID int6
|
||||
return groupID, nil
|
||||
}
|
||||
|
||||
// RoomGroupID 生成腾讯云 IM 房间群 ID。
|
||||
func RoomGroupID(value string) (string, error) {
|
||||
return RoomGroupIDWithPrefix("", value)
|
||||
}
|
||||
|
||||
// RoomGroupIDWithPrefix 生成带环境前缀的腾讯云 IM 房间群 ID。
|
||||
func RoomGroupIDWithPrefix(prefix string, value string) (string, error) {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if err := validateGroupIDPrefix(prefix); err != nil {
|
||||
return "", err
|
||||
}
|
||||
room := strings.TrimSpace(value)
|
||||
if !roomid.ValidStringID(room) {
|
||||
return "", fmt.Errorf("room_id is invalid for group_id")
|
||||
}
|
||||
// 腾讯云自定义 GroupID 不接受 UUID 连字符;内部 room_id 保持不变,只在 IM 边界转义。
|
||||
groupID := prefix + strings.ReplaceAll(room, "-", "_")
|
||||
if len(groupID) > roomid.MaxStringIDLength {
|
||||
return "", fmt.Errorf("room group_id is too long")
|
||||
}
|
||||
return groupID, nil
|
||||
}
|
||||
|
||||
// Parse 在调用任何 room guard 前完成 GroupID 分类。
|
||||
// 解析顺序必须先播报群、后房间群;否则 hy_<app>_bc_* 这类合法短字符串可能被 roomid 规则吞掉。
|
||||
func Parse(groupID string) ParsedGroup {
|
||||
@ -101,8 +124,8 @@ func Parse(groupID string) ParsedGroup {
|
||||
return ParsedGroup{Kind: KindUnknown}
|
||||
}
|
||||
|
||||
if roomid.ValidStringID(groupID) {
|
||||
return ParsedGroup{Kind: KindRoom, RoomID: groupID}
|
||||
if roomID, ok := RoomIDFromRoomGroupID(groupID); ok {
|
||||
return ParsedGroup{Kind: KindRoom, RoomID: roomID}
|
||||
}
|
||||
return ParsedGroup{Kind: KindUnknown}
|
||||
}
|
||||
@ -118,17 +141,75 @@ func ParseWithPrefix(groupID string, prefix string) ParsedGroup {
|
||||
return Parse(groupID)
|
||||
}
|
||||
if after, ok := strings.CutPrefix(groupID, prefix); ok {
|
||||
return parseBroadcastGroupID(after)
|
||||
if parsed := parseBroadcastGroupID(after); parsed.Kind != KindUnknown {
|
||||
return parsed
|
||||
}
|
||||
if roomID, ok := RoomIDFromRoomGroupID(after); ok {
|
||||
return ParsedGroup{Kind: KindRoom, RoomID: roomID}
|
||||
}
|
||||
return ParsedGroup{Kind: KindUnknown}
|
||||
}
|
||||
if strings.HasPrefix(groupID, "hy_") && strings.Contains(groupID, "_bc_") {
|
||||
return ParsedGroup{Kind: KindUnknown}
|
||||
}
|
||||
if roomid.ValidStringID(groupID) {
|
||||
return ParsedGroup{Kind: KindRoom, RoomID: groupID}
|
||||
if roomID, ok := RoomIDFromRoomGroupID(groupID); ok {
|
||||
return ParsedGroup{Kind: KindRoom, RoomID: roomID}
|
||||
}
|
||||
return ParsedGroup{Kind: KindUnknown}
|
||||
}
|
||||
|
||||
// RoomIDFromRoomGroupID 把腾讯云 IM 房间群 ID 还原成内部 room_id。
|
||||
func RoomIDFromRoomGroupID(groupID string) (string, bool) {
|
||||
groupID = strings.TrimSpace(groupID)
|
||||
if !roomid.ValidStringID(groupID) {
|
||||
return "", false
|
||||
}
|
||||
if roomID, ok := uuidRoomGroupIDToRoomID(groupID); ok {
|
||||
return roomID, true
|
||||
}
|
||||
return groupID, true
|
||||
}
|
||||
|
||||
func uuidRoomGroupIDToRoomID(groupID string) (string, bool) {
|
||||
parts := strings.Split(groupID, "_")
|
||||
if len(parts) < 6 {
|
||||
return "", false
|
||||
}
|
||||
tail := parts[len(parts)-5:]
|
||||
if !isHexLength(tail[0], 8) || !isHexLength(tail[1], 4) || !isHexLength(tail[2], 4) || !isHexLength(tail[3], 4) || !isHexLength(tail[4], 12) {
|
||||
return "", false
|
||||
}
|
||||
prefix := strings.Join(parts[:len(parts)-5], "_")
|
||||
if prefix == "" {
|
||||
return "", false
|
||||
}
|
||||
roomID := prefix + "_" + tail[0] + "-" + tail[1] + "-" + tail[2] + "-" + tail[3] + "-" + tail[4]
|
||||
if !roomid.ValidStringID(roomID) {
|
||||
return "", false
|
||||
}
|
||||
return roomID, true
|
||||
}
|
||||
|
||||
func isHexLength(value string, length int) bool {
|
||||
if len(value) != length {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(value); i++ {
|
||||
char := value[i]
|
||||
if char >= '0' && char <= '9' {
|
||||
continue
|
||||
}
|
||||
if char >= 'a' && char <= 'f' {
|
||||
continue
|
||||
}
|
||||
if char >= 'A' && char <= 'F' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseBroadcastGroupID(groupID string) ParsedGroup {
|
||||
if strings.HasPrefix(groupID, "hy_") && strings.HasSuffix(groupID, globalSuffix) {
|
||||
app := strings.TrimSuffix(strings.TrimPrefix(groupID, "hy_"), globalSuffix)
|
||||
|
||||
@ -43,6 +43,21 @@ func TestParseRoomAndRejectInvalidBroadcast(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomGroupIDEscapesUUIDHyphen(t *testing.T) {
|
||||
roomID := "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca"
|
||||
groupID, err := RoomGroupID(roomID)
|
||||
if err != nil {
|
||||
t.Fatalf("RoomGroupID failed: %v", err)
|
||||
}
|
||||
if groupID != "lalu_2963d975_316e_444d_ad5c_f82fc36c28ca" {
|
||||
t.Fatalf("room group mismatch: %s", groupID)
|
||||
}
|
||||
parsed := Parse(groupID)
|
||||
if parsed.Kind != KindRoom || parsed.RoomID != roomID {
|
||||
t.Fatalf("room group parse mismatch: %+v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastGroupIDWithPrefix(t *testing.T) {
|
||||
global, err := GlobalBroadcastGroupIDWithPrefix("test_", "lalu")
|
||||
if err != nil {
|
||||
@ -73,4 +88,11 @@ func TestBroadcastGroupIDWithPrefix(t *testing.T) {
|
||||
if parsed := ParseWithPrefix("room-1001", "test_"); parsed.Kind != KindRoom || parsed.RoomID != "room-1001" {
|
||||
t.Fatalf("room parse with prefix mismatch: %+v", parsed)
|
||||
}
|
||||
prefixedRoom, err := RoomGroupIDWithPrefix("test_", "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca")
|
||||
if err != nil {
|
||||
t.Fatalf("RoomGroupIDWithPrefix failed: %v", err)
|
||||
}
|
||||
if parsed := ParseWithPrefix(prefixedRoom, "test_"); parsed.Kind != KindRoom || parsed.RoomID != "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
|
||||
t.Fatalf("prefixed room parse mismatch: %+v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ type AccountProfile struct {
|
||||
|
||||
// RoomEvent 是 room-service 发给腾讯 IM 的房间系统消息负载。
|
||||
type RoomEvent struct {
|
||||
GroupID string `json:"-"`
|
||||
EventID string `json:"event_id"`
|
||||
RoomID string `json:"room_id"`
|
||||
EventType string `json:"event_type"`
|
||||
@ -220,6 +221,10 @@ func (c *RESTClient) PublishRoomEvent(ctx context.Context, event RoomEvent) erro
|
||||
// event_id 是客户端去重、日志追踪和补偿投递判断的共同锚点。
|
||||
return fmt.Errorf("room event is incomplete")
|
||||
}
|
||||
groupID := strings.TrimSpace(event.GroupID)
|
||||
if groupID == "" {
|
||||
groupID = event.RoomID
|
||||
}
|
||||
|
||||
eventPayload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
@ -227,7 +232,7 @@ func (c *RESTClient) PublishRoomEvent(ctx context.Context, event RoomEvent) erro
|
||||
}
|
||||
|
||||
return c.PublishGroupCustomMessage(ctx, CustomGroupMessage{
|
||||
GroupID: event.RoomID,
|
||||
GroupID: groupID,
|
||||
EventID: event.EventID,
|
||||
Desc: event.EventType,
|
||||
Ext: "room_system_message",
|
||||
|
||||
@ -58,6 +58,7 @@ func TestRESTClientPublishRoomEventBuildsCustomMessage(t *testing.T) {
|
||||
})
|
||||
|
||||
err := client.PublishRoomEvent(context.Background(), RoomEvent{
|
||||
GroupID: "room_1001",
|
||||
EventID: "evt-1",
|
||||
RoomID: "room-1001",
|
||||
EventType: "room_gift_sent",
|
||||
@ -73,7 +74,7 @@ func TestRESTClientPublishRoomEventBuildsCustomMessage(t *testing.T) {
|
||||
if capturedPath != "/"+sendGroupMsgCommand {
|
||||
t.Fatalf("path mismatch: got %q", capturedPath)
|
||||
}
|
||||
if capturedBody["GroupId"] != "room-1001" {
|
||||
if capturedBody["GroupId"] != "room_1001" {
|
||||
t.Fatalf("group id mismatch: %+v", capturedBody)
|
||||
}
|
||||
body, ok := capturedBody["MsgBody"].([]any)
|
||||
@ -91,6 +92,9 @@ func TestRESTClientPublishRoomEventBuildsCustomMessage(t *testing.T) {
|
||||
if !strings.Contains(content["Data"].(string), `"event_id":"evt-1"`) {
|
||||
t.Fatalf("event payload missing event_id: %s", content["Data"])
|
||||
}
|
||||
if !strings.Contains(content["Data"].(string), `"room_id":"room-1001"`) {
|
||||
t.Fatalf("event payload must keep internal room_id: %s", content["Data"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRESTClientPublishGroupCustomMessageBuildsBroadcastPayload(t *testing.T) {
|
||||
|
||||
@ -58,7 +58,7 @@ rocketmq:
|
||||
secret_key: ""
|
||||
namespace: ""
|
||||
room_outbox:
|
||||
enabled: false
|
||||
enabled: true
|
||||
topic: "hyapp_room_outbox"
|
||||
consumer_group: "hyapp-activity-room-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
|
||||
@ -2471,7 +2471,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
}
|
||||
rooms := data["rooms"].([]any)
|
||||
first := rooms[0].(map[string]any)
|
||||
if first["im_group_id"] != "room-1" || first["locked"] != true || first["country_code"] != "US" {
|
||||
if first["im_group_id"] != "room_1" || first["locked"] != true || first["country_code"] != "US" {
|
||||
t.Fatalf("room list must expose room IM group id: %+v", first)
|
||||
}
|
||||
}
|
||||
@ -2836,7 +2836,7 @@ func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
|
||||
t.Fatalf("my room must preserve has_room=true: %+v", data)
|
||||
}
|
||||
room := data["room"].(map[string]any)
|
||||
if room["room_id"] != "room-owner" || room["im_group_id"] != "room-owner" {
|
||||
if room["room_id"] != "room-owner" || room["im_group_id"] != "room_owner" {
|
||||
t.Fatalf("my room must expose room id and im group id: %+v", room)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,10 +72,10 @@ func TestCreateRoomDataIncludesExplicitIMGroupID(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
if data.Room.RoomID != "room-create-im" || data.Room.IMGroupID != "room-create-im" {
|
||||
if data.Room.RoomID != "room-create-im" || data.Room.IMGroupID != "room_create_im" {
|
||||
t.Fatalf("create response room must expose room_id and im_group_id: %+v", data.Room)
|
||||
}
|
||||
if data.IM.GroupID != "room-create-im" || !data.IM.NeedJoinGroup {
|
||||
if data.IM.GroupID != "room_create_im" || !data.IM.NeedJoinGroup {
|
||||
t.Fatalf("create response must expose joinable im group: %+v", data.IM)
|
||||
}
|
||||
if !data.Result.Applied || data.ServerTimeMS != 1700000000000 {
|
||||
|
||||
@ -3,6 +3,7 @@ package roomapi
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"hyapp/pkg/imgroup"
|
||||
"hyapp/pkg/tencentrtc"
|
||||
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
@ -1085,8 +1086,12 @@ func roomSeatCounts(snapshot *roomv1.RoomSnapshot) (int32, int32) {
|
||||
}
|
||||
|
||||
func roomIMGroupID(roomID string) string {
|
||||
// 当前房间群规则固定为腾讯云 IM GroupID 等于内部 room_id;显式下发给客户端,避免端侧硬编码规则。
|
||||
return roomID
|
||||
// 房间业务 ID 保持原始值;腾讯 IM GroupID 在协议边界转义,避免 UUID 连字符被腾讯 REST 拒绝。
|
||||
groupID, err := imgroup.RoomGroupID(roomID)
|
||||
if err != nil {
|
||||
return roomID
|
||||
}
|
||||
return groupID
|
||||
}
|
||||
|
||||
func formatOptionalUserID(userID int64) string {
|
||||
|
||||
@ -68,7 +68,7 @@ rocketmq:
|
||||
tencent_im_consumer_group: "hyapp-room-im-bridge"
|
||||
consumer_max_reconsume_times: 16
|
||||
rocket_launch:
|
||||
enabled: false
|
||||
enabled: true
|
||||
topic: "hyapp_room_rocket_launch"
|
||||
producer_group: "hyapp-room-rocket-launch-producer"
|
||||
consumer_group: "hyapp-room-rocket-launch"
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
"hyapp/pkg/imgroup"
|
||||
"hyapp/pkg/tencentim"
|
||||
)
|
||||
|
||||
@ -28,7 +29,11 @@ func (p *TencentIMPublisher) EnsureRoomGroup(ctx context.Context, roomID string,
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.client.EnsureRoomGroup(ctx, roomID, ownerUserID)
|
||||
groupID, err := imgroup.RoomGroupID(roomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.client.EnsureRoomGroup(ctx, groupID, ownerUserID)
|
||||
}
|
||||
|
||||
// PublishRoomEvent 把房间系统事件发送到腾讯云 IM 群。
|
||||
@ -38,6 +43,11 @@ func (p *TencentIMPublisher) PublishRoomEvent(ctx context.Context, event tencent
|
||||
return nil
|
||||
}
|
||||
|
||||
groupID, err := imgroup.RoomGroupID(event.RoomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.GroupID = groupID
|
||||
return p.client.PublishRoomEvent(ctx, event)
|
||||
}
|
||||
|
||||
@ -52,7 +62,7 @@ func (p *TencentIMPublisher) PublishOutboxEvent(ctx context.Context, envelope *r
|
||||
return err
|
||||
}
|
||||
// 建群是腾讯 IM 的外部副作用,必须留在 outbox worker 内,不能阻塞 CreateRoom 返回。
|
||||
return p.client.EnsureRoomGroup(ctx, envelope.GetRoomId(), body.GetOwnerUserId())
|
||||
return p.EnsureRoomGroup(ctx, envelope.GetRoomId(), body.GetOwnerUserId())
|
||||
}
|
||||
|
||||
event, publish, err := roomEventFromEnvelope(envelope)
|
||||
@ -61,7 +71,11 @@ func (p *TencentIMPublisher) PublishOutboxEvent(ctx context.Context, envelope *r
|
||||
}
|
||||
if envelope.GetEventType() == "RoomUserKicked" {
|
||||
// KickUser 的安全边界除了系统消息,还必须补偿移除腾讯云 IM 群成员。
|
||||
if err := p.client.DeleteRoomGroupMember(ctx, envelope.GetRoomId(), event.TargetUserID); err != nil {
|
||||
groupID, err := imgroup.RoomGroupID(envelope.GetRoomId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.client.DeleteGroupMember(ctx, groupID, event.TargetUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user