syntax = "proto3";

package hyapp.room.v1;

option go_package = "hyapp.local/api/proto/room/v1;roomv1";

// RequestMeta 固定承载所有命令的调用元信息。
// room-service 用它串起幂等、追踪、路由和业务操作者身份。
message RequestMeta {
  string request_id = 1;
  string command_id = 2;
  int64 actor_user_id = 3;
  // room_id 是命令路由、持久化和腾讯 IM/RTC 映射共同依赖的必填字段。
  // CreateRoom 时必须满足 ^[A-Za-z0-9_-]{1,48}$。
  string room_id = 4;
  string gateway_node_id = 5;
  string session_id = 6;
  int64 sent_at_ms = 7;
  string app_code = 8;
  // device_id 只能来自 gateway 已验签 access JWT 的同名 claim；旧 JWT 缺字段时保持空值，不得用 session_id/command_id 伪造。
  string device_id = 9;
}

// CommandResult 统一返回命令是否真正落地，以及落地后的房间版本。
message CommandResult {
  bool applied = 1;
  int64 room_version = 2;
  int64 server_time_ms = 3;
}

// RoomUser 只表达 room-service 需要保存的房间内轻量用户态。
message RoomUser {
  int64 user_id = 1;
  string role = 2;
  int64 joined_at_ms = 3;
  int64 last_seen_at_ms = 4;
  int64 gift_value = 5;
}

// RoomOnlineUser 是在线用户列表的展示读模型。
// role 保留进房 presence 语义；room_role 只表达房间管理员身份；is_owner 单独表达房主身份。
message RoomOnlineUser {
  int64 user_id = 1;
  string role = 2;
  string room_role = 3;
  int64 gift_value = 4;
  int64 joined_at_ms = 5;
  int64 last_seen_at_ms = 6;
  bool is_owner = 7;
}

// SeatState 表达单个麦位当前占用和 RTC 发流确认状态。
message SeatState {
  int32 seat_no = 1;
  int64 user_id = 2;
  bool locked = 3;
  // publish_state 为空表示空麦或未进入发流流程；上麦后先进入 pending_publish，确认后进入 publishing。
  string publish_state = 4;
  // mic_session_id 是单次上麦发流会话 ID；所有客户端确认或 RTC webhook 必须带回它。
  string mic_session_id = 5;
  // publish_deadline_ms 是 pending_publish 必须确认发流的截止时间。
  int64 publish_deadline_ms = 6;
  // mic_session_room_version 是创建本次 mic_session 的房间版本，用于丢弃旧版本 RTC 事件。
  int64 mic_session_room_version = 7;
  // last_publish_event_time_ms 记录已接受的最新 RTC/客户端发流事件时间。
  int64 last_publish_event_time_ms = 8;
  // mic_muted 是服务端可见的麦克风静音状态，用于同步其他用户 UI。
  bool mic_muted = 9;
  // seat_status 是服务端派生的展示状态：empty/locked/occupied/publishing/muted。
  string seat_status = 10;
  // mic_heartbeat_at_ms 是当前 mic_session 最近一次服务端接受麦上心跳的 UTC epoch ms。
  int64 mic_heartbeat_at_ms = 11;
  // gift_value 是当前房间内该麦位用户收到的礼物热度累计，用户离房后清零。
  int64 gift_value = 12;
}

// RankItem 表达房间内本地礼物榜项目。
message RankItem {
  int64 user_id = 1;
  int64 score = 2;
  int64 gift_value = 3;
  int64 updated_at_ms = 4;
}

// RoomContributionRankItem 表达房间内某个周期的送礼贡献用户排行项。
message RoomContributionRankItem {
  int64 rank = 1;
  int64 user_id = 2;
  int64 score = 3;
  int64 gift_value = 4;
  int64 updated_at_ms = 5;
}

// LuckyGiftDrawResult 是 SendGift 同步返回给当前送礼用户的幸运礼物抽奖表现。
message LuckyGiftDrawResult {
  bool enabled = 1;
  string draw_id = 2;
  string command_id = 3;
  string pool_id = 4;
  string gift_id = 5;
  int64 rule_version = 6;
  string experience_pool = 7;
  string selected_tier_id = 8;
  int64 multiplier_ppm = 9;
  int64 base_reward_coins = 10;
  int64 effective_reward_coins = 11;
  string reward_status = 12;
  bool stage_feedback = 13;
  bool high_multiplier = 14;
  int64 created_at_ms = 15;
  string wallet_transaction_id = 16;
  int64 coin_balance_after = 17;
  // target_user_id 是本次幸运礼物抽奖对应的收礼用户；多目标送礼用它关联结果。
  int64 target_user_id = 18;
  // hits 只包含 gift_count 微批中实际中奖的位置；旧客户端忽略该追加字段。
  repeated SendGiftLuckyHit hits = 19;
}

// RoomRocketRewardItem 是后台配置给客户端展示的火箭奖励候选项。
message RoomRocketRewardItem {
  string reward_item_id = 1;
  int64 resource_group_id = 2;
  int64 weight = 3;
  string display_name = 4;
  string icon_url = 5;
  // quantity 表示该奖项在一次火箭发射中最多发给多少个在房用户；为空或 0 时兼容旧配置按 1 份处理。
  int64 quantity = 6;
}

// RoomRocketLevel 暴露单个等级火箭的物料、阈值和三类奖励池。
message RoomRocketLevel {
  int32 level = 1;
  int64 fuel_threshold = 2;
  string cover_url = 3;
  string animation_url = 4;
  string launch_animation_url = 5;
  string launched_image_url = 6;
  repeated RoomRocketRewardItem in_room_rewards = 7;
  repeated RoomRocketRewardItem top1_rewards = 8;
  repeated RoomRocketRewardItem igniter_rewards = 9;
}

// RoomRocketRewardGrant 是一次火箭发射后给某个用户结算出的具体奖励。
message RoomRocketRewardGrant {
  string reward_role = 1;
  int64 user_id = 2;
  string reward_item_id = 3;
  int64 resource_group_id = 4;
  string display_name = 5;
  string icon_url = 6;
  string grant_id = 7;
  string status = 8;
}

// RoomRocketContribution 是当前火箭等级内的实际加燃料贡献累计。
message RoomRocketContribution {
  int64 user_id = 1;
  int64 fuel = 2;
  int64 updated_at_ms = 3;
}

// RoomRocketPendingLaunch 是已经点火、等待延迟发射的单枚火箭。
message RoomRocketPendingLaunch {
  string rocket_id = 1;
  int32 level = 2;
  int64 fuel_threshold = 3;
  int64 ignited_at_ms = 4;
  int64 launch_at_ms = 5;
  int64 reset_at_ms = 6;
  int64 top1_user_id = 7;
  int64 igniter_user_id = 8;
  int64 config_version = 9;
  string cover_url = 10;
}

// RoomRocketState 是 Room Cell 持有并随快照恢复的当前火箭状态。
message RoomRocketState {
  int32 current_level = 1;
  int64 current_fuel = 2;
  int64 fuel_threshold = 3;
  string status = 4;
  int64 ignited_at_ms = 5;
  int64 launch_at_ms = 6;
  int64 launched_at_ms = 7;
  int64 reset_at_ms = 8;
  int64 top1_user_id = 9;
  int64 igniter_user_id = 10;
  string rocket_id = 11;
  int64 config_version = 12;
  repeated RoomRocketRewardGrant last_rewards = 13;
  repeated RoomRocketPendingLaunch pending_launches = 14;
  repeated RoomRocketContribution current_contributions = 15;
}

// RoomRocketInfo 是 App 初始化火箭 UI 的完整读模型。
message RoomRocketInfo {
  bool enabled = 1;
  repeated RoomRocketLevel levels = 2;
  RoomRocketState state = 3;
  int64 server_time_ms = 4;
  string broadcast_scope = 5;
  int64 launch_delay_ms = 6;
  int64 broadcast_delay_ms = 7;
  string reward_stack_policy = 8;
}

// RoomRocketGiftFuelRule 是后台配置的礼物燃料覆盖规则。
message RoomRocketGiftFuelRule {
  string rule_id = 1;
  string gift_id = 2;
  string gift_type_code = 3;
  int64 multiplier_ppm = 4;
  int64 fixed_fuel = 5;
  bool excluded = 6;
}

// AdminRoomRocketConfig 是后台管理读写的完整火箭配置。
message AdminRoomRocketConfig {
  string app_code = 1;
  bool enabled = 2;
  int64 config_version = 3;
  string fuel_source = 4;
  int64 launch_delay_ms = 5;
  bool broadcast_enabled = 6;
  string broadcast_scope = 7;
  int64 broadcast_delay_ms = 8;
  string reward_stack_policy = 9;
  repeated RoomRocketLevel levels = 10;
  repeated RoomRocketGiftFuelRule gift_fuel_rules = 11;
  int64 updated_by_admin_id = 12;
  int64 created_at_ms = 13;
  int64 updated_at_ms = 14;
}

message AdminGetRoomRocketConfigRequest {
  RequestMeta meta = 1;
}

message AdminGetRoomRocketConfigResponse {
  AdminRoomRocketConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminUpdateRoomRocketConfigRequest {
  RequestMeta meta = 1;
  AdminRoomRocketConfig config = 2;
  int64 admin_id = 3;
}

message AdminUpdateRoomRocketConfigResponse {
  AdminRoomRocketConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminRoomSeatConfig {
  repeated int32 candidate_seat_counts = 1;
  repeated int32 allowed_seat_counts = 2;
  int32 default_seat_count = 3;
  int64 updated_at_ms = 4;
}

message AdminGetRoomSeatConfigRequest {
  RequestMeta meta = 1;
}

message AdminGetRoomSeatConfigResponse {
  AdminRoomSeatConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminUpdateRoomSeatConfigRequest {
  RequestMeta meta = 1;
  repeated int32 allowed_seat_counts = 2;
  int32 default_seat_count = 3;
}

message AdminUpdateRoomSeatConfigResponse {
  AdminRoomSeatConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminHumanRoomRobotCountryPool {
  string country_code = 1;
  repeated int64 robot_user_ids = 2;
}

message AdminHumanRoomRobotCountryRule {
  string country_code = 1;
  int32 max_room_count = 2;
  // allowed_owner_ids 为空表示该国家扫描所有真人房；非空时只扫描房主展示短号/内部 owner_user_id 命中的房间。
  repeated string allowed_owner_ids = 3;
}

message AdminHumanRoomRobotConfig {
  string app_code = 1;
  bool enabled = 2;
  int32 candidate_room_max_online = 3;
  int32 room_full_stop_online = 4;
  int64 robot_stay_min_ms = 5;
  int64 robot_stay_max_ms = 6;
  int64 robot_replace_min_ms = 7;
  int64 robot_replace_max_ms = 8;
  int64 normal_gift_interval_ms = 9;
  repeated string gift_ids = 10;
  repeated string lucky_gift_ids = 11;
  repeated string super_lucky_gift_ids = 12;
  int64 lucky_combo_min = 13;
  int64 lucky_combo_max = 14;
  int64 lucky_pause_min_ms = 15;
  int64 lucky_pause_max_ms = 16;
  int64 max_gift_senders = 17;
  repeated AdminHumanRoomRobotCountryPool country_pools = 18;
  uint64 updated_by_admin_id = 19;
  int64 created_at_ms = 20;
  int64 updated_at_ms = 21;
  int64 normal_gift_interval_min_ms = 22;
  int64 normal_gift_interval_max_ms = 23;
  int32 room_target_min_online = 24;
  int32 room_target_max_online = 25;
  // allowed_owner_ids 为空表示扫描所有真人房；非空时只扫描房主展示短号/内部 owner_user_id 命中的房间。
  repeated string allowed_owner_ids = 26;
  // country_limit_enabled 打开后只按 country_rules 中配置的国家进真人房。
  bool country_limit_enabled = 27;
  // country_rules 配置每个国家最多同时进入几个真人房；空集合表示不进入任何国家。
  repeated AdminHumanRoomRobotCountryRule country_rules = 28;
}

message AdminGetHumanRoomRobotConfigRequest {
  RequestMeta meta = 1;
}

message AdminGetHumanRoomRobotConfigResponse {
  AdminHumanRoomRobotConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminUpdateHumanRoomRobotConfigRequest {
  RequestMeta meta = 1;
  AdminHumanRoomRobotConfig config = 2;
  uint64 admin_id = 3;
}

message AdminUpdateHumanRoomRobotConfigResponse {
  AdminHumanRoomRobotConfig config = 1;
  int64 server_time_ms = 2;
}

message AdminRoomPinRoom {
  string room_id = 1;
  string room_short_id = 2;
  int64 visible_region_id = 3;
  int64 owner_user_id = 4;
  string title = 5;
  string cover_url = 6;
  string status = 7;
}

message AdminRoomPin {
  int64 id = 1;
  int64 visible_region_id = 2;
  string room_id = 3;
  int64 weight = 4;
  string status = 5;
  int64 pinned_at_ms = 6;
  int64 expires_at_ms = 7;
  int64 cancelled_at_ms = 8;
  uint64 created_by_admin_id = 9;
  uint64 cancelled_by_admin_id = 10;
  int64 created_at_ms = 11;
  int64 updated_at_ms = 12;
  AdminRoomPinRoom room = 13;
  string pin_type = 14;
}

message AdminListRoomPinsRequest {
  RequestMeta meta = 1;
  int32 page = 2;
  int32 page_size = 3;
  string keyword = 4;
  string status = 5;
  int64 visible_region_id = 6;
  string pin_type = 7;
}

message AdminListRoomPinsResponse {
  repeated AdminRoomPin pins = 1;
  int64 total = 2;
  int64 server_time_ms = 3;
}

message AdminCreateRoomPinRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 weight = 3;
  int64 duration_days = 4;
  uint64 admin_id = 5;
  int64 pinned_at_ms = 6;
  int64 expires_at_ms = 7;
  string pin_type = 8;
}

message AdminCreateRoomPinResponse {
  AdminRoomPin pin = 1;
  int64 server_time_ms = 2;
}

message AdminCancelRoomPinRequest {
  RequestMeta meta = 1;
  int64 pin_id = 2;
  uint64 admin_id = 3;
}

message AdminCancelRoomPinResponse {
  AdminRoomPin pin = 1;
  int64 server_time_ms = 2;
}

message AdminRobotRoomGiftRule {
  repeated string gift_ids = 1;
  repeated string lucky_gift_ids = 2;
  int64 normal_gift_interval_ms = 3;
  int64 lucky_combo_min = 4;
  int64 lucky_combo_max = 5;
  int64 lucky_pause_min_ms = 6;
  int64 lucky_pause_max_ms = 7;
  int64 robot_stay_min_ms = 8;
  int64 robot_stay_max_ms = 9;
  int64 robot_replace_min_ms = 10;
  int64 robot_replace_max_ms = 11;
  int64 max_gift_senders = 12;
}

message AdminRobotRoom {
  string app_code = 1;
  string room_id = 2;
  string room_short_id = 3;
  string title = 4;
  string cover_url = 5;
  int64 visible_region_id = 6;
  string status = 7;
  int64 owner_robot_user_id = 8;
  repeated int64 robot_user_ids = 9;
  AdminRobotRoomGiftRule gift_rule = 10;
  uint64 created_by_admin_id = 11;
  int64 created_at_ms = 12;
  int64 updated_at_ms = 13;
  int32 active_robot_count = 14;
  int32 seat_count = 15;
}

message AdminListRobotRoomsRequest {
  RequestMeta meta = 1;
  int32 page = 2;
  int32 page_size = 3;
  string status = 4;
}

message AdminListRobotRoomsResponse {
  repeated AdminRobotRoom rooms = 1;
  int64 total = 2;
  int64 server_time_ms = 3;
}

message AdminCreateRobotRoomRequest {
  RequestMeta meta = 1;
  int64 owner_robot_user_id = 2;
  repeated int64 candidate_robot_user_ids = 3;
  int32 min_robot_count = 4;
  int32 max_robot_count = 5;
  string room_name = 6;
  string room_avatar = 7;
  int64 visible_region_id = 8;
  AdminRobotRoomGiftRule gift_rule = 9;
  uint64 admin_id = 10;
  string owner_country_code = 11;
  int32 seat_count = 12;
}

message AdminCreateRobotRoomResponse {
  AdminRobotRoom room = 1;
  int64 server_time_ms = 2;
}

message AdminSetRobotRoomStatusRequest {
  RequestMeta meta = 1;
  string status = 2;
  uint64 admin_id = 3;
}

message AdminSetRobotRoomStatusResponse {
  AdminRobotRoom room = 1;
  int64 server_time_ms = 2;
}

message AdminRenameOwnerCountryCodeRequest {
  RequestMeta meta = 1;
  string old_country_code = 2;
  string new_country_code = 3;
  uint64 admin_id = 4;
}

message AdminRenameOwnerCountryCodeResponse {
  int64 room_list_entries = 1;
  int64 room_snapshots = 2;
  int64 server_time_ms = 3;
}

message AdminFilterAvailableRoomRobotsRequest {
  RequestMeta meta = 1;
  repeated int64 user_ids = 2;
}

message AdminFilterAvailableRoomRobotsResponse {
  repeated int64 available_user_ids = 1;
  repeated int64 occupied_user_ids = 2;
  int64 server_time_ms = 3;
}

// RoomBanState 表达单个房间 ban 的治理状态。
message RoomBanState {
  int64 user_id = 1;
  int64 actor_user_id = 2;
  int64 created_at_ms = 3;
  // expires_at_ms 为 0 表示永久 ban；非 0 表示该 UTC 毫秒后可重新进房。
  int64 expires_at_ms = 4;
}

// RoomSnapshot 把 room-service 对外需要暴露的房间投影集中返回。
message RoomSnapshot {
  string room_id = 1;
  int64 owner_user_id = 2;
  string mode = 4;
  string status = 5;
  bool chat_enabled = 6;
  repeated SeatState mic_seats = 7;
  repeated RoomUser online_users = 8;
  repeated int64 admin_user_ids = 9;
  repeated int64 ban_user_ids = 10;
  repeated int64 mute_user_ids = 11;
  repeated RankItem gift_rank = 12;
  map<string, string> room_ext = 13;
  int64 heat = 14;
  int64 version = 15;
  string app_code = 16;
  string room_short_id = 17;
  int64 visible_region_id = 18;
  // locked 只表达房间是否需要入房密码；具体密码哈希不对外映射到 HTTP JSON。
  bool locked = 19;
  // rocket 是语音房火箭当前状态；配置物料通过 GetRoomRocket 单独读取，避免快照过大。
  RoomRocketState rocket = 20;
  // ban_states 保存 ban 的过期边界；ban_user_ids 仍只服务简单集合判断。
  repeated RoomBanState ban_states = 21;
  // online_count 允许 lite 响应不携带 online_users 明细仍保留准确在线人数。
  int32 online_count = 22;
  // room_border 与 room_name_style 是房间级最终装扮快照；客户端不得再按房主 VIP 等级推导样式。
  RoomDecorationResource room_border = 23;
  RoomDecorationResource room_name_style = 24;
  // active_background 固化当前背景的真实媒体属性；room_ext.background_url 仅保留旧客户端兼容。
  RoomBackgroundImage active_background = 25;
  // room_border_url 是当前有效 room_border.asset_url 的扁平兼容字段；空字符串表示没有有效房间边框。
  string room_border_url = 26;
}

// RoomMedia 是房间背景和房间图片消息共同使用的可信媒体快照。
// URL 只能由 gateway 的受信对象存储上传入口生成；room-service 会再次校验 object_key 归属和媒体边界。
message RoomMedia {
  string url = 1;
  string object_key = 2;
  string content_type = 3;
  string format = 4;
  int64 size_bytes = 5;
  int32 width = 6;
  int32 height = 7;
  bool animated = 8;
  int32 frame_count = 9;
  int64 duration_ms = 10;
  string poster_url = 11;
  string thumbnail_url = 12;
  string sha256 = 13;
  string status = 14;
}

// RoomBackgroundImage 是房间维度保存过的背景图素材。
// 保存列表归属于 room-service，当前生效背景仍写入 RoomSnapshot.room_ext["background_url"]。
message RoomBackgroundImage {
  int64 background_id = 1;
  string room_id = 2;
  string image_url = 3;
  int64 created_by_user_id = 4;
  int64 created_at_ms = 5;
  int64 updated_at_ms = 6;
  RoomMedia media = 7;
  string command_id = 8;
}

message SaveRoomBackgroundRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  // image_url 仅兼容旧内部调用；新版专用上传必须传完整 media，保存时不按 URL 后缀猜格式。
  string image_url = 3;
  RoomMedia media = 4;
}

message SaveRoomBackgroundResponse {
  RoomBackgroundImage background = 1;
  int64 server_time_ms = 2;
}

message ListRoomBackgroundsRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 viewer_user_id = 3;
}

message ListRoomBackgroundsResponse {
  repeated RoomBackgroundImage backgrounds = 1;
  string selected_background_url = 2;
  int64 server_time_ms = 3;
  RoomBackgroundImage selected_background = 4;
}

message SetRoomBackgroundRequest {
  RequestMeta meta = 1;
  int64 background_id = 2;
}

message SetRoomBackgroundResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
  RoomBackgroundImage background = 3;
}

// RoomDecorationResource 是 wallet 资源目录和有效 entitlement 在房间应用时固化的展示快照。
// 资源后续编辑、下架或 VIP 到期不会篡改历史 command log；新的读取只返回当前 Room Cell 已应用结果。
message RoomDecorationResource {
  int64 resource_id = 1;
  string resource_code = 2;
  string resource_type = 3;
  string name = 4;
  string asset_url = 5;
  string preview_url = 6;
  string animation_url = 7;
  string format = 8;
  string metadata_json = 9;
  string entitlement_id = 10;
  repeated string text_colors = 11;
  repeated double stops = 12;
  double angle_degrees = 13;
  int64 expires_at_ms = 14;
}

message ApplyRoomDecorationRequest {
  RequestMeta meta = 1;
  // decoration_type 仅接受 room_border/colored_room_name；资源类型分别固定为 room_border/room_name_style。
  string decoration_type = 2;
  int64 resource_id = 3;
  string entitlement_id = 4;
}

message ApplyRoomDecorationResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
  string decoration_type = 3;
  RoomDecorationResource resource = 4;
  int64 server_time_ms = 5;
}

message AuthorizeRoomMediaUploadRequest {
  RequestMeta meta = 1;
  // purpose 接受 room_background/room_image/room_cover；授权发生在 COS PutObject 之前。
  string purpose = 2;
  RoomMedia media = 3;
}

message AuthorizeRoomMediaUploadResponse {
  bool allowed = 1;
  string object_key_prefix = 2;
  int64 room_version = 3;
  int64 server_time_ms = 4;
  // object_key 由 room owner 按首次 command_id+媒体哈希确定，gateway 不能自行换 key。
  string object_key = 5;
  // cover_upload_id 仅 room_cover 返回；创建/更新房间必须提交该凭证，不能提交上传 URL。
  string cover_upload_id = 6;
}

message CompleteRoomMediaUploadRequest {
  RequestMeta meta = 1;
  string purpose = 2;
  // media 必须携带 PutObject 成功后的 url/object_key，并与首次 Authorize 的解析事实完全一致。
  RoomMedia media = 3;
}

message CompleteRoomMediaUploadResponse {
  bool active = 1;
  RoomMedia media = 2;
  int64 server_time_ms = 3;
  // cover_upload_id 仅 room_cover 返回，并与 Authorize 首次生成的凭证完全一致。
  string cover_upload_id = 4;
}

message SendRoomImageRequest {
  RequestMeta meta = 1;
  RoomMedia image = 2;
}

message SendRoomImageResponse {
  CommandResult result = 1;
  bool accepted = 2;
  string message_id = 3;
  string room_id = 4;
  int64 sender_user_id = 5;
  int64 server_time_ms = 6;
  RoomMedia image = 7;
}

// CreateRoomRequest 创建一个新的房间执行单元。
// 必填语义：meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 cover_upload_id。
// 同一个 meta.actor_user_id 只能作为 owner 创建一个房间；重复创建返回 Conflict。
message CreateRoomRequest {
  RequestMeta meta = 1;
  // seat_count 为空或 0 时使用后台房间配置默认值；正数必须命中后台启用座位数。
  int32 seat_count = 2;
  // mode 必须非空；当前只作为房间状态字段保存和透出。
  string mode = 3;
  // visible_region_id 由 gateway 按创建者 user-service region_id 填充；0 表示 GLOBAL 兜底列表桶。
  int64 visible_region_id = 4;
  // room_name 是客户端创建房间时填写的展示名称，room-service 会写入 RoomSnapshot.room_ext["title"]。
  string room_name = 5;
  // room_avatar 仅保留 legacy_timed App 的历史协议；权益制 App 禁止客户端直接提交 URL。
  string room_avatar = 6;
  // room_description 是房间简介，只作为房间扩展资料保存，不参与 Room Cell 高频状态决策。
  string room_description = 7;
  // room_short_id 是房间短 ID，首版直接等于创建者当前 default/current display_user_id。
  string room_short_id = 8;
  // owner_country_code 由 gateway 按创建者 user-service 当前国家填充，用于发现页国家分类筛选。
  string owner_country_code = 9;
  // robot_room 只供内部机器人房间编排器设置；gateway 不接受客户端提交该字段。
  bool robot_room = 10;
  // robot_user_ids 是该机器人房间允许参与机器人送礼和虚拟上麦的用户集合。
  repeated int64 robot_user_ids = 11;
  // cover_upload_id 来自 POST /api/v1/rooms/covers/upload，消费后不能用于另一间房或另一次资料更新。
  string cover_upload_id = 12;
}

// CreateRoomResponse 返回新建后的房间快照。
message CreateRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// UpdateRoomProfileRequest 修改房间展示资料和麦位数量。
// 该命令只允许当前在房间 presence 内的 owner 执行，座位数必须命中后台启用配置。
message UpdateRoomProfileRequest {
  RequestMeta meta = 1;
  optional string room_name = 2;
  // room_avatar 仅保留 legacy_timed App 的历史协议；权益制 App 使用 cover_upload_id。
  optional string room_avatar = 3;
  optional string room_description = 4;
  optional int32 seat_count = 5;
  // cover_upload_id 与 room_avatar 互斥；room-service 在资料命令事务内消费该凭证。
  optional string cover_upload_id = 6;
}

// UpdateRoomProfileResponse 返回修改后的最新房间快照。
message UpdateRoomProfileResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// JoinRoomRequest 把用户 presence 接入房间业务态。
message RoomEntryVehicleSnapshot {
  int64 resource_id = 1;
  string resource_code = 2;
  string name = 3;
  string asset_url = 4;
  string preview_url = 5;
  string animation_url = 6;
  string metadata_json = 7;
  string entitlement_id = 8;
  int64 expires_at_ms = 9;
}

// RoomUserDisplayProfile 是 gateway 在入口固化的用户展示快照，只服务 IM/UI 兜底。
message RoomUserDisplayProfile {
  int64 user_id = 1;
  string nickname = 2;
  string avatar = 3;
  string display_user_id = 4;
  string pretty_display_user_id = 5;
}

message JoinRoomRequest {
  RequestMeta meta = 1;
  string role = 2;
  // password 只用于本次入房校验；room-service 不把明文写入 command log 或快照。
  string password = 3;
  RoomEntryVehicleSnapshot entry_vehicle = 4;
  int64 actor_country_id = 5;
  int64 actor_region_id = 6;
  // actor_is_robot 只由 room-service 内部机器人调度链路设置，外部 gateway 进房保持 false。
  bool actor_is_robot = 7;
  // actor_display_profile 是进房用户展示资料快照，room-service 只透传到进房 IM/outbox，不写入 RoomState。
  RoomUserDisplayProfile actor_display_profile = 8;
  // response_mode=lite 只裁剪响应快照，不改变 Room Cell 命令提交、outbox 或 IM 事件语义。
  string response_mode = 9;
  // presence_recovery 只用于客户端仍在房间但业务 presence 已超时清理的恢复。
  // 恢复成功只补回 Room Cell 在线状态，不发布 RoomUserJoined IM/outbox，避免重复入场表现和统计。
  bool presence_recovery = 10;
}

// RoomEffectiveVipSnapshot 是 room-service 在进房时从 wallet-service 固化的轻量 VIP 展示事实。
// 权益执行仍以后端 effective_benefits 为准，客户端不能用该快照自行声明房间权限。
message RoomEffectiveVipSnapshot {
  string program_type = 1;
  int32 level = 2;
  string name = 3;
  bool room_entry_notice_enabled = 4;
}

// JoinRoomResponse 返回加入后的房间快照。
message JoinRoomResponse {
  CommandResult result = 1;
  RoomUser user = 2;
  RoomSnapshot room = 3;
  RoomEffectiveVipSnapshot effective_vip = 4;
}

// RoomHeartbeatRequest 显式刷新已经存在的房间业务 presence。
// 它不能替代 JoinRoom；用户不在房间时必须拒绝，避免心跳把已离开的用户重新加入房间。
message RoomHeartbeatRequest {
  RequestMeta meta = 1;
}

// RoomHeartbeatResponse 返回刷新后的用户 presence 和房间快照。
message RoomHeartbeatResponse {
  CommandResult result = 1;
  RoomUser user = 2;
  RoomSnapshot room = 3;
}

// LeaveRoomRequest 把用户从房间 presence 移出。
message LeaveRoomRequest {
  RequestMeta meta = 1;
}

// LeaveRoomResponse 返回离房后的房间快照。
message LeaveRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// CloseRoomRequest 关闭房间并清理业务 presence 和麦位。
message CloseRoomRequest {
  RequestMeta meta = 1;
  string reason = 2;
}

// CloseRoomResponse 返回关闭后的房间快照。
message CloseRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// AdminRoomListItem 是后台房间管理卡片；它来自 room-service owner 读模型，不允许后台直连房间库。
message AdminRoomListItem {
  string room_id = 1;
  string room_short_id = 2;
  int64 visible_region_id = 3;
  int64 owner_user_id = 4;
  string title = 5;
  string cover_url = 6;
  string mode = 7;
  string status = 8;
  string close_reason = 9;
  uint64 closed_by_admin_id = 10;
  string closed_by_admin_name = 11;
  int64 closed_at_ms = 12;
  int64 heat = 13;
  int32 online_count = 14;
  int32 seat_count = 15;
  int32 occupied_seat_count = 16;
  int64 sort_score = 17;
  int64 created_at_ms = 18;
  int64 updated_at_ms = 19;
  // locked 只暴露是否需要入房密码，供内部展示投影生成可安全点击的房间入口。
  bool locked = 20;
}

message AdminListRoomsRequest {
  RequestMeta meta = 1;
  int32 page = 2;
  int32 page_size = 3;
  string keyword = 4;
  string status = 5;
  int64 visible_region_id = 6;
  string sort_by = 7;
  string sort_direction = 8;
  int64 owner_user_id = 9;
  // owner_user_ids 由可信后台 facade 注入，用于在 count/分页前限制到外管递归团队。
  repeated int64 owner_user_ids = 10;
}

message AdminListRoomsResponse {
  repeated AdminRoomListItem rooms = 1;
  int64 total = 2;
  int64 server_time_ms = 3;
}

message AdminGetRoomRequest {
  RequestMeta meta = 1;
  string room_id = 2;
}

message AdminGetRoomResponse {
  AdminRoomListItem room = 1;
  int64 server_time_ms = 2;
}

message AdminUpdateRoomRequest {
  RequestMeta meta = 1;
  optional string title = 2;
  optional string cover_url = 3;
  optional string description = 4;
  optional string mode = 5;
  optional string status = 6;
  optional int64 visible_region_id = 7;
  string close_reason = 8;
  uint64 admin_id = 9;
  string admin_name = 10;
  // owner_country_code 是内部运维/同步字段，用于通过真实 Room Cell owner 修复房主国家投影。
  optional string owner_country_code = 11;
}

message AdminUpdateRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

message AdminBanRoomRequest {
  RequestMeta meta = 1;
  string reason = 2;
  uint64 admin_id = 3;
  string admin_name = 4;
}

message AdminBanRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

message AdminUnbanRoomRequest {
  RequestMeta meta = 1;
  string reason = 2;
  uint64 admin_id = 3;
  string admin_name = 4;
}

message AdminUnbanRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

message AdminDeleteRoomRequest {
  RequestMeta meta = 1;
  uint64 admin_id = 2;
  string admin_name = 3;
}

message AdminDeleteRoomResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// MicUpRequest 申请把用户放到指定麦位。
message MicUpRequest {
  RequestMeta meta = 1;
  int32 seat_no = 2;
  // target_display_profile 是上麦用户展示快照，room-service 只透传到麦位 IM/outbox。
  RoomUserDisplayProfile target_display_profile = 3;
}

// MicUpResponse 返回最新房间快照与目标麦位。
message MicUpResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
  string mic_session_id = 4;
  int64 publish_deadline_ms = 5;
}

// MicDownRequest 把用户从当前麦位移下。
message MicDownRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  // reason 为空表示主动下麦；系统自动释放麦位时会写入稳定原因，例如 publish_timeout。
  string reason = 3;
  // target_display_profile 只用于补偿 IM 展示，空值不影响下麦状态提交。
  RoomUserDisplayProfile target_display_profile = 4;
}

// MicDownResponse 返回最新房间快照与目标麦位。
message MicDownResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
}

// ChangeMicSeatRequest 直接调整指定用户所在麦位。
message ChangeMicSeatRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  int32 seat_no = 3;
  // target_display_profile 是被换麦用户展示快照，避免客户端等待在线成员刷新。
  RoomUserDisplayProfile target_display_profile = 4;
}

// ChangeMicSeatResponse 返回变更后的房间快照。
message ChangeMicSeatResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// ConfirmMicPublishingRequest 确认当前 mic_session 已经在 RTC 侧成功发布音频。
// 客户端 SDK 回调和后续 RTC webhook 都必须带 mic_session_id、room_version 和 event_time_ms。
message ConfirmMicPublishingRequest {
  RequestMeta meta = 1;
  // target_user_id 为空时默认确认 actor_user_id；RTC webhook 入口可显式指定目标用户。
  int64 target_user_id = 2;
  string mic_session_id = 3;
  int64 room_version = 4;
  int64 event_time_ms = 5;
  string source = 6;
  // target_display_profile 是确认发流用户展示快照，空值不影响状态收敛。
  RoomUserDisplayProfile target_display_profile = 7;
}

// ConfirmMicPublishingResponse 返回确认后的最新房间快照。
message ConfirmMicPublishingResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
}

// MicHeartbeatRequest 刷新当前 publishing 麦位会话的服务端麦上心跳。
message MicHeartbeatRequest {
  RequestMeta meta = 1;
  // target_user_id 为空时默认刷新 actor_user_id 自己的麦上会话。
  int64 target_user_id = 2;
  // mic_session_id 必须匹配当前麦位上的会话，避免旧心跳续住新会话。
  string mic_session_id = 3;
}

// MicHeartbeatResponse 返回刷新后的麦位和房间快照。
message MicHeartbeatResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
  int64 mic_heartbeat_at_ms = 4;
}

// SetMicMuteRequest 修改麦位上的服务端可见静音态。
message SetMicMuteRequest {
  RequestMeta meta = 1;
  // target_user_id 为空时默认修改 actor_user_id 自己的麦克风静音态。
  int64 target_user_id = 2;
  bool muted = 3;
  // target_display_profile 是被静音状态变更用户展示快照，只服务 IM/UI 兜底。
  RoomUserDisplayProfile target_display_profile = 4;
}

// SetMicMuteResponse 返回静音态变更后的最新房间快照。
message SetMicMuteResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
}

// ApplyRTCEventRequest 把可信 RTC 服务端回调转换成 Room Cell 命令。
// event_type 当前只接受 audio_started、audio_stopped、room_exited。
message ApplyRTCEventRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  string event_type = 3;
  int64 event_time_ms = 4;
  string reason = 5;
  string source = 6;
  string external_event_id = 7;
}

// ApplyRTCEventResponse 返回 RTC 事件落房间状态后的快照。
message ApplyRTCEventResponse {
  CommandResult result = 1;
  int32 seat_no = 2;
  RoomSnapshot room = 3;
}

// SetMicSeatLockRequest 锁定或解锁指定麦位。
message SetMicSeatLockRequest {
  RequestMeta meta = 1;
  int32 seat_no = 2;
  bool locked = 3;
}

// SetMicSeatLockResponse 返回锁麦后的最新房间快照。
message SetMicSeatLockResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// SetChatEnabledRequest 开启或关闭房间公屏。
message SetChatEnabledRequest {
  RequestMeta meta = 1;
  bool enabled = 2;
}

// SetChatEnabledResponse 返回聊天开关变更后的最新房间快照。
message SetChatEnabledResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// SetRoomPasswordRequest 设置或清空房间入房密码。
// locked=true 时 password 必须非空；locked=false 表示清空密码并解除锁房。
message SetRoomPasswordRequest {
  RequestMeta meta = 1;
  bool locked = 2;
  string password = 3;
}

// SetRoomPasswordResponse 返回锁房状态变更后的最新房间快照。
message SetRoomPasswordResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// SetRoomAdminRequest 添加或移除房间管理员。
message SetRoomAdminRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  bool enabled = 3;
}

// SetRoomAdminResponse 返回管理员集合变更后的最新房间快照。
message SetRoomAdminResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// MuteUserRequest 修改房间内禁言态。
message MuteUserRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  bool muted = 3;
}

// MuteUserResponse 返回最新房间快照。
message MuteUserResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// KickUserRequest 把指定用户踢出房间。
message KickUserRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  // duration_ms 为 0 表示永久 ban；大于 0 表示从服务端当前 UTC 时间起禁止进房的时长。
  int64 duration_ms = 3;
}

// KickUserResponse 返回最新房间快照。
message KickUserResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
  // rtc_kicked 表示 room-service 已通过 TRTC 服务端接口移除目标用户；失败不回滚房间踢人事实。
  bool rtc_kicked = 3;
  // rtc_kick_error 仅用于调用方排障和告警，客户端展示仍以统一错误码和房间状态为准。
  string rtc_kick_error = 4;
}

// UnbanUserRequest 解除房间内 ban，用户仍需重新 JoinRoom。
message UnbanUserRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
}

// UnbanUserResponse 返回解封后的最新房间快照。
message UnbanUserResponse {
  CommandResult result = 1;
  RoomSnapshot room = 2;
}

// SystemEvictUserRequest 是平台级封禁、风控或后台治理触发的系统驱逐入口。
// 该命令仍由 Room Cell 修改房间状态；调用方不能直接写房间 presence 或麦位。
message SystemEvictUserRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  int64 operator_user_id = 3;
  string reason = 4;
  // ban_from_room 为 true 时把用户加入当前房间 ban 集合，防止同一房间恢复入口重新进入。
  bool ban_from_room = 5;
}

// SystemEvictUserResponse 返回系统驱逐的房间状态和外部 RTC 副作用结果。
message SystemEvictUserResponse {
  bool had_current_room = 1;
  CommandResult result = 2;
  RoomSnapshot room = 3;
  string room_id = 4;
  bool rtc_kicked = 5;
  string rtc_kick_error = 6;
}

// SendGiftTargetHostScope 是 gateway 在送礼入口按每个接收方固化的主播工资入账快照。
message SendGiftTargetHostScope {
  int64 target_user_id = 1;
  // target_is_host 由 gateway 根据 user-service active host profile 注入，客户端不能直接声明。
  bool target_is_host = 2;
  // target_host_region_id 是主播所属区域，后续结算按该区域匹配 Host & Agency 政策。
  int64 target_host_region_id = 3;
  // target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户；为空表示当前无代理或代理关系不可结算。
  int64 target_agency_owner_user_id = 4;
}

// SendGiftDisplayProfile 是 gateway 在送礼入口固化的轻量展示快照。
// room-service 只把它写入事件和 IM，不把用户资料扩散到 Room Cell 核心状态。
message SendGiftDisplayProfile {
  int64 user_id = 1;
  string username = 2;
  string avatar = 3;
  string display_user_id = 4;
  string pretty_display_user_id = 5;
  // effective_vip_level 是送礼入口查询到的当前生效 VIP 等级；0 表示当前无有效 VIP 或展示查询已降级。
  int32 effective_vip_level = 6;
  // gift_broadcast_animation_url 是 wallet 已校验的有效 gift_tray_skin 动效快照；空值表示无可渲染托盘或查询降级。
  string gift_broadcast_animation_url = 7;
}

// SendGiftBatchTarget 是批量送礼展示响应中单个目标的结算和展示快照。
// 它只服务发送方本地即时展示，不替代逐目标 RoomGiftSent 业务事实。
message SendGiftBatchTarget {
  int64 target_user_id = 1;
  string receiver_nickname = 2;
  string receiver_avatar = 3;
  string receiver_display_user_id = 4;
  string receiver_pretty_display_user_id = 5;
  int64 gift_value = 6;
  int64 target_gift_value = 7;
  int64 coin_spent = 8;
  string billing_receipt_id = 9;
  string command_id = 10;
  LuckyGiftDrawResult lucky_gift = 11;
}

// SendGiftBatchDisplay 是新批量送礼接口返回给发送方的本地展示协议。
// 房间广播使用同构的 RoomGiftBatchSent IM 事件；coin_balance_after 不进入该展示对象。
message SendGiftBatchDisplay {
  string event_id = 1;
  int64 sender_user_id = 2;
  string sender_name = 3;
  string sender_avatar = 4;
  string sender_display_user_id = 5;
  string sender_pretty_display_user_id = 6;
  string gift_id = 7;
  int32 gift_count = 8;
  int64 total_gift_value = 9;
  int64 total_coin_spent = 10;
  string billing_receipt_id = 11;
  int64 room_heat = 12;
  string pool_id = 13;
  string gift_type_code = 14;
  string cp_relation_type = 15;
  string gift_name = 16;
  string gift_icon_url = 17;
  string gift_animation_url = 18;
  repeated string gift_effect_types = 19;
  repeated int64 target_user_ids = 20;
  repeated SendGiftBatchTarget targets = 21;
  string command_id = 22;
  // sender_vip_level 与 RoomGiftBatchSent IM 使用同一份入站快照，避免发送方本地展示和房间广播不一致。
  int32 sender_vip_level = 23;
  // sender_gift_broadcast_animation_url 与逐目标 RoomGiftSent 共用送礼入口快照，普通/幸运/超级幸运礼物保持同一托盘资源。
  string sender_gift_broadcast_animation_url = 24;
}

// SendGiftRequest 是首版房间内最重要的变现命令。
message SendGiftRequest {
  RequestMeta meta = 1;
  // target_user_id 是兼容旧客户端的单目标字段；新客户端优先使用 target_user_ids。
  int64 target_user_id = 2;
  string gift_id = 3;
  int32 gift_count = 4;
  // target_type 当前只提交 user；后续扩展 all_mic、all_room、couple 时不再改 HTTP 契约。
  string target_type = 5;
  repeated int64 target_user_ids = 6;
  string pool_id = 7;
  // target_is_host 由 gateway 根据 user-service active host profile 注入，room-service 只透传给 wallet-service。
  bool target_is_host = 8;
  // target_host_region_id 是主播所属区域，后续结算按该区域匹配 Host & Agency 政策。
  int64 target_host_region_id = 9;
  // target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户；为空表示当前无代理或代理关系不可结算。
  int64 target_agency_owner_user_id = 10;
  // sender_region_id 是 gateway 从 user-service 当前送礼用户资料注入的区域，用于 wallet 匹配礼物钻石比例。
  int64 sender_region_id = 11;
  // sender_country_id 是 gateway 从 user-service 当前送礼用户资料注入的真实国家，用于统计国家维度。
  int64 sender_country_id = 12;
  // entitlement_id 非空表示从用户 Bag 礼物库存中消费，不走金币扣费。
  string entitlement_id = 13;
  // source 当前支持 bag；为空按普通金币礼物兼容。
  string source = 14;
  // target_host_scopes 覆盖批量 target 的工资入账快照；单目标旧字段仍保留兼容老调用链。
  repeated SendGiftTargetHostScope target_host_scopes = 15;
  // sender_display_profile 是 gateway 从 user-service 读取的送礼人展示快照，用于 IM 兜底展示。
  SendGiftDisplayProfile sender_display_profile = 16;
  // target_display_profiles 是每个收礼目标的展示快照；多目标 IM 按 target_user_id 取对应项。
  repeated SendGiftDisplayProfile target_display_profiles = 17;
  // display_mode=batch 只由新 batch-send HTTP 入口设置，用来生成单条多人展示 IM；旧 send 入口保持空值。
  string display_mode = 18;
  // combo_session_id + batch_seq 只描述 Flutter 微批会话，command_id 仍是跨服务唯一幂等键。
  string combo_session_id = 19;
  int64 batch_seq = 20;
  // sender_registered_at_ms 是 gateway 从 user-service 读取的账号创建事实；客户端不能提交或覆盖。
  // room-service 只随首次送礼命令持久化并透传给 lucky-gift-service，不据此拥有用户主数据。
  int64 sender_registered_at_ms = 21;
}

// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
message SendGiftResponse {
  CommandResult result = 1;
  string billing_receipt_id = 2;
  int64 room_heat = 3;
  repeated RankItem gift_rank = 4;
  RoomSnapshot room = 5;
  RoomRocketState rocket = 6;
  // lucky_gift 是本次送礼的聚合幸运礼物结果；多目标时倍率和返奖金额按所有目标累加。
  LuckyGiftDrawResult lucky_gift = 7;
  // lucky_gifts 是每个收礼目标的独立抽奖结果；多目标客户端用 target_user_id 对齐明细表现。
  repeated LuckyGiftDrawResult lucky_gifts = 8;
  // coin_balance_after 是送礼人本次扣费和幸运礼物返奖完成后的 COIN 可用余额，只返回给送礼发起方。
  int64 coin_balance_after = 9;
  // gift_income_balance_after 是收礼人返币后的 COIN 余额；多目标或无返币时为 0。
  int64 gift_income_balance_after = 10;
  // batch_display 是新 batch-send 接口的多人展示快照；旧 send 入口保持空值。
  SendGiftBatchDisplay batch_display = 11;
  // v2 保存并重放首次成功的原始结算结果；追加字段不改变旧 Flutter 对顶层 V1 字段的读取。
  SendGiftResultV2 v2 = 12;
}

// CheckSpeakPermissionRequest 让腾讯云 IM 发言回调或 gateway 在公屏前同步问房间业务态。
message CheckSpeakPermissionRequest {
  string room_id = 1;
  int64 user_id = 2;
  string app_code = 3;
}

// CheckSpeakPermissionResponse 返回当前用户是否允许发言。
message CheckSpeakPermissionResponse {
  bool allowed = 1;
  string reason = 2;
  int64 room_version = 3;
}

// VerifyRoomPresenceRequest 让外部 IM 入口在进群前确认用户业务上仍在房间内。
message VerifyRoomPresenceRequest {
  string room_id = 1;
  int64 user_id = 2;
  string session_id = 3;
  string request_id = 4;
  string app_code = 5;
}

// VerifyRoomPresenceResponse 返回用户是否仍然属于该房间。
message VerifyRoomPresenceResponse {
  bool present = 1;
  string reason = 2;
  int64 room_version = 3;
}

// ResolveRoomAppCodeRequest 仅供已验签的三方回调按全局 room_id 解析租户。
// 该入口不接受客户端 app_code，避免固定 URL 参数把 Fami 事件错误路由到 Lalu。
message ResolveRoomAppCodeRequest {
  string request_id = 1;
  string room_id = 2;
}

message ResolveRoomAppCodeResponse {
  string app_code = 1;
}

// ListRoomsRequest 查询首页公共房间列表。
// visible_region_id 是 gateway 解析的实际查询区域：默认取 viewer 区域，显式筛选时取已校验的 filter_region_id。
// gateway 同时写 filter_region_id 绑定游标；客户端不能直接构造任一内部字段。
message ListRoomsRequest {
  RequestMeta meta = 1;
  int64 viewer_user_id = 2;
  int64 visible_region_id = 3;
  string tab = 4;
  string cursor = 5;
  int32 limit = 6;
  string query = 7;
  // country_code 是可选国家筛选值；gateway 必须先确认它属于 active 区域。
  string country_code = 8;
  // viewer_country_code 是当前用户国家，用于 All/区域列表的本国家优先排序。
  string viewer_country_code = 9;
  // all_visible_regions 只能由 gateway 根据 App 策略或后台白名单注入；为 true 时列表不按 visible_region_id 过滤。
  bool all_visible_regions = 10;
  // filter_region_id 是 gateway 校验过的 active 区域筛选；它与 all_visible_regions/country_code 互斥。
  int64 filter_region_id = 11;
}

// ListRoomsByOwnersRequest 查询一组房主名下 active 房间。
// owner_user_ids 必须由 gateway 根据上游业务归属注入，客户端不能直接提交任意房主集合。
message ListRoomsByOwnersRequest {
  RequestMeta meta = 1;
  int64 viewer_user_id = 2;
  repeated int64 owner_user_ids = 3;
  string tab = 4;
  string cursor = 5;
  int32 limit = 6;
  string query = 7;
}

// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following/followed 房间流。
// 它和公共房间发现列表分离，避免把用户关系流误建模成房间全集过滤。
message ListRoomFeedsRequest {
  RequestMeta meta = 1;
  int64 viewer_user_id = 2;
  int64 visible_region_id = 3;
  string tab = 4;
  string cursor = 5;
  int32 limit = 6;
  string query = 7;
  // related_users 只用于 friend/following；关系事实由 gateway 从 user-service 读取后传入。
  repeated RoomFeedRelatedUser related_users = 8;
}

// ListRoomGiftLeaderboardRequest 查询当前 UTC 周期内的房间金币消耗榜。
message ListRoomGiftLeaderboardRequest {
  RequestMeta meta = 1;
  string period = 2;
  int32 page = 3;
  int32 page_size = 4;
  // region_id 由 gateway 从登录用户资料注入；Lalu/Fami 必填，客户端 query 不可信。
  int64 region_id = 5;
}

// RoomFeedRelatedUser 是 Mine 关系房间流的排序输入，不让 room-service 持有好友/关注事实。
message RoomFeedRelatedUser {
  int64 user_id = 1;
  int64 relation_updated_at_ms = 2;
}

// RoomListItem 是房间发现页卡片读模型，不承载完整 Room Cell 状态。
message RoomListItem {
  string room_id = 1;
  int64 owner_user_id = 2;
  string title = 4;
  string cover_url = 5;
  string mode = 6;
  string status = 7;
  int64 heat = 8;
  int32 online_count = 9;
  int32 seat_count = 10;
  int32 occupied_seat_count = 11;
  int64 visible_region_id = 12;
  string app_code = 13;
  string room_short_id = 14;
  bool locked = 15;
  string country_code = 16;
  RoomDecorationResource room_border = 17;
  RoomDecorationResource room_name_style = 18;
  // owner_vip_level 是 wallet effective VIP 的异步列表投影；0 表示房主当前没有有效 VIP。
  // room-service 在创建/房主进房和 wallet outbox 变化时刷新，列表查询本身不回查 wallet。
  int32 owner_vip_level = 19;
  // room_border_url 直接从同一列表投影中的 room_border.asset_url 派生，不增加列表查询或 wallet 回查。
  string room_border_url = 20;
  // rocket_near_completion 来自 Room Cell 火箭快照；仅 charging 且剩余燃料不超过当前档阈值 20% 时为 true。
  bool rocket_near_completion = 21;
  // has_active_red_packet 是 wallet 红包事实经 MQ 异步投影的房间聚合，不触发列表同步 RPC 或逐房查询。
  bool has_active_red_packet = 22;
  // active_red_packet_expires_at_ms 是当前仍有余量红包的最晚到期时间；客户端缓存可据此在边界后立即隐藏图标。
  int64 active_red_packet_expires_at_ms = 23;
}

// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
message ListRoomsResponse {
  repeated RoomListItem rooms = 1;
  string next_cursor = 2;
}

// RoomGiftLeaderboardItem 是跨房间金币消耗榜卡片；score 只表达该周期房间礼物金币消耗。
message RoomGiftLeaderboardItem {
  int64 rank = 1;
  string room_id = 2;
  int64 coin_spent = 3;
  RoomListItem room = 4;
}

// ListRoomGiftLeaderboardResponse 返回当前 UTC 周期房间金币榜。
message ListRoomGiftLeaderboardResponse {
  repeated RoomGiftLeaderboardItem items = 1;
  int64 total = 2;
  string period = 3;
  int64 start_at_ms = 4;
  int64 end_at_ms = 5;
  int64 server_time_ms = 6;
}

// ListRoomContributionRankRequest 查询单房间内用户送礼贡献周期榜。
message ListRoomContributionRankRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  string period = 3;
  int32 limit = 4;
  int64 viewer_user_id = 5;
}

// ListRoomContributionRankResponse 返回单房间内用户送礼贡献周期榜。
message ListRoomContributionRankResponse {
  string room_id = 1;
  string period = 2;
  int64 start_at_ms = 3;
  int64 end_at_ms = 4;
  repeated RoomContributionRankItem items = 5;
  int64 total = 6;
  int64 server_time_ms = 7;
}

// GetMyRoomRequest 查询当前用户自己创建的房间。
// 该查询必须由 gateway 写入鉴权后的 owner_user_id，不能从客户端接收用户 ID。
message GetMyRoomRequest {
  RequestMeta meta = 1;
  int64 owner_user_id = 2;
}

// GetMyRoomResponse 返回 Mine 顶部“我的房间”权威卡片。
// 它直接读取 rooms 元数据并用 Room Cell 快照补充实时卡片字段，不依赖发现页投影是否已经写入。
message GetMyRoomResponse {
  bool has_room = 1;
  RoomListItem room = 2;
  int64 server_time_ms = 3;
}

// GetCurrentRoomRequest 查询当前用户是否仍有可恢复的房间 presence。
// 该查询必须由 gateway 写入鉴权后的 user_id，客户端不能提交 user_id。
message GetCurrentRoomRequest {
  RequestMeta meta = 1;
  int64 user_id = 2;
}

// GetCurrentRoomResponse 是 App 启动、回前台和网络恢复时的房间恢复探测结果。
// 它只表达是否需要恢复，不会隐式 JoinRoom 或刷新 presence。
message GetCurrentRoomResponse {
  bool has_current_room = 1;
  string room_id = 2;
  int64 room_version = 3;
  string role = 4;
  string mic_session_id = 5;
  string publish_state = 6;
  bool need_join_im_group = 7;
  bool need_rtc_token = 8;
  int64 server_time_ms = 9;
}

// BatchGetUserVoiceRoomPresencesRequest 查询一组用户当前可向 viewer 展示的上麦入口。
// user_ids 只能由 gateway 从外部查询参数解析；room-service 仍会去重并限制最多 100 个。
message BatchGetUserVoiceRoomPresencesRequest {
  RequestMeta meta = 1;
  int64 viewer_user_id = 2;
  repeated int64 user_ids = 3;
}

// UserVoiceRoomPresenceRoom 是个人资料页 Live 入口需要的最小房间投影。
// 它不携带在线用户和完整麦位集合，避免 batch 查询把 RoomSnapshot 明细扩散到用户域接口。
message UserVoiceRoomPresenceRoom {
  string room_id = 1;
  string room_short_id = 2;
  int64 owner_user_id = 3;
  // 当前房间模型没有独立主持人字段，host_user_id 与 owner_user_id 保持一致。
  int64 host_user_id = 4;
  string title = 5;
  string cover_url = 6;
  string background_url = 7;
  string mode = 8;
  int32 online_count = 9;
  int32 seat_count = 10;
  bool locked = 11;
}

// UserVoiceRoomPresence 对每个去重后的请求用户返回一条稳定结果。
// 不可见、不可进入和未上麦统一返回 not_on_mic，避免通过差异状态探测房间位置。
message UserVoiceRoomPresence {
  int64 user_id = 1;
  string state = 2;
  int32 seat_no = 3;
  bool joinable = 4;
  int64 observed_at_ms = 5;
  UserVoiceRoomPresenceRoom room = 6;
}

message BatchGetUserVoiceRoomPresencesResponse {
  repeated UserVoiceRoomPresence items = 1;
  int64 server_time_ms = 2;
}

// GetRoomSnapshotRequest 主动读取当前房间完整快照。
// 该查询必须由 gateway 写入鉴权后的 viewer_user_id，客户端不能提交 viewer_user_id。
message GetRoomSnapshotRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 viewer_user_id = 3;
}

// GetRoomSnapshotResponse 返回 Room Cell 当前快照；只读，不刷新 presence。
message GetRoomSnapshotResponse {
  RoomSnapshot room = 1;
  int64 server_time_ms = 2;
  // is_followed 表达 viewer 是否已经关注该房间；它来自 room_follows 关系表，不属于 Room Cell 核心快照。
  bool is_followed = 3;
}

// GetRoomRocketRequest 主动读取当前房间火箭配置物料和进度状态。
// 该查询必须由 gateway 写入鉴权后的 viewer_user_id，客户端不能提交 viewer_user_id。
message GetRoomRocketRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 viewer_user_id = 3;
}

message GetRoomRocketResponse {
  RoomRocketInfo rocket = 1;
  int64 server_time_ms = 2;
}

// ApplyRoomRPSGiftRequest 只承载 room-rps 最终结算后真实发生的一份败者礼物。
// game-service 的预扣、平局退款和赢家本金退款都不能调用该接口。
message ApplyRoomRPSGiftRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 sender_user_id = 3;
  int64 target_user_id = 4;
  string gift_id = 5;
  int64 heat_value = 6;
}

message ApplyRoomRPSGiftResponse {
  CommandResult result = 1;
  RoomRocketState rocket = 2;
}

// ListRoomOnlineUsersRequest 分页查询房间在线用户，不让客户端拉完整 RoomSnapshot 做列表分页。
message ListRoomOnlineUsersRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 viewer_user_id = 3;
  int32 page = 4;
  int32 page_size = 5;
  string sort = 6;
}

message ListRoomOnlineUsersResponse {
  repeated RoomUser users = 1;
  int64 total = 2;
  int32 page = 3;
  int32 page_size = 4;
  int64 server_time_ms = 5;
  repeated RoomOnlineUser items = 6;
}

// RoomBannedUser 是房间黑名单列表的轻量治理读模型；用户展示资料由 gateway 批量补齐。
message RoomBannedUser {
  int64 user_id = 1;
  int64 actor_user_id = 2;
  int64 created_at_ms = 3;
  // unban_at_ms 为 0 表示永久 ban；非 0 表示该 UTC 毫秒后自动允许重新进房。
  int64 unban_at_ms = 4;
  int64 remaining_ms = 5;
  bool permanent = 6;
}

// ListRoomBannedUsersRequest 分页查询房间当前仍有效的黑名单。
message ListRoomBannedUsersRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 viewer_user_id = 3;
  int32 page = 4;
  int32 page_size = 5;
}

message ListRoomBannedUsersResponse {
  repeated RoomBannedUser items = 1;
  int64 total = 2;
  int32 page = 3;
  int32 page_size = 4;
  int64 server_time_ms = 5;
}

// FollowRoomRequest 建立当前用户对房间的关注关系。
// 该关系是 room-service 的用户-房间低频关系事实，不进入 Room Cell command log。
message FollowRoomRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 user_id = 3;
}

message FollowRoomResponse {
  string room_id = 1;
  bool followed = 2;
  int64 followed_at_ms = 3;
  int64 server_time_ms = 4;
}

// UnfollowRoomRequest 取消当前用户对房间的关注关系；未关注时按幂等成功处理。
message UnfollowRoomRequest {
  RequestMeta meta = 1;
  string room_id = 2;
  int64 user_id = 3;
}

message UnfollowRoomResponse {
  string room_id = 1;
  bool followed = 2;
  int64 server_time_ms = 3;
}

// RoomCommandService 承载所有会改变房间状态的命令。
service RoomCommandService {
  rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
  rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
  rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse);
  rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse);
  rpc ApplyRoomDecoration(ApplyRoomDecorationRequest) returns (ApplyRoomDecorationResponse);
  rpc SendRoomImage(SendRoomImageRequest) returns (SendRoomImageResponse);
  rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
  rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
  rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);
  rpc CloseRoom(CloseRoomRequest) returns (CloseRoomResponse);
  rpc AdminUpdateRoom(AdminUpdateRoomRequest) returns (AdminUpdateRoomResponse);
  rpc AdminBanRoom(AdminBanRoomRequest) returns (AdminBanRoomResponse);
  rpc AdminUnbanRoom(AdminUnbanRoomRequest) returns (AdminUnbanRoomResponse);
  rpc AdminDeleteRoom(AdminDeleteRoomRequest) returns (AdminDeleteRoomResponse);
  rpc AdminUpdateRoomRocketConfig(AdminUpdateRoomRocketConfigRequest) returns (AdminUpdateRoomRocketConfigResponse);
  rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse);
  rpc AdminUpdateHumanRoomRobotConfig(AdminUpdateHumanRoomRobotConfigRequest) returns (AdminUpdateHumanRoomRobotConfigResponse);
  rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse);
  rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse);
  rpc AdminCreateRobotRoom(AdminCreateRobotRoomRequest) returns (AdminCreateRobotRoomResponse);
  rpc AdminSetRobotRoomStatus(AdminSetRobotRoomStatusRequest) returns (AdminSetRobotRoomStatusResponse);
  rpc AdminRenameOwnerCountryCode(AdminRenameOwnerCountryCodeRequest) returns (AdminRenameOwnerCountryCodeResponse);
  rpc MicUp(MicUpRequest) returns (MicUpResponse);
  rpc MicDown(MicDownRequest) returns (MicDownResponse);
  rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
  rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse);
  rpc MicHeartbeat(MicHeartbeatRequest) returns (MicHeartbeatResponse);
  rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse);
  rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
  rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
  rpc SetChatEnabled(SetChatEnabledRequest) returns (SetChatEnabledResponse);
  rpc SetRoomPassword(SetRoomPasswordRequest) returns (SetRoomPasswordResponse);
  rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
  rpc MuteUser(MuteUserRequest) returns (MuteUserResponse);
  rpc KickUser(KickUserRequest) returns (KickUserResponse);
  rpc UnbanUser(UnbanUserRequest) returns (UnbanUserResponse);
  rpc SystemEvictUser(SystemEvictUserRequest) returns (SystemEvictUserResponse);
  rpc SendGift(SendGiftRequest) returns (SendGiftResponse);
  // ApplyRoomRPSGift 由 game-service 在赢家退款成功后调用；Room Cell 仍是火箭状态唯一 owner。
  rpc ApplyRoomRPSGift(ApplyRoomRPSGiftRequest) returns (ApplyRoomRPSGiftResponse);
  rpc FollowRoom(FollowRoomRequest) returns (FollowRoomResponse);
  rpc UnfollowRoom(UnfollowRoomRequest) returns (UnfollowRoomResponse);
}

// RoomGuardService 提供给腾讯云 IM 回调或 gateway 做实时守卫查询。
service RoomGuardService {
  rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse);
  rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse);
  rpc ResolveRoomAppCode(ResolveRoomAppCodeRequest) returns (ResolveRoomAppCodeResponse);
  rpc AuthorizeRoomMediaUpload(AuthorizeRoomMediaUploadRequest) returns (AuthorizeRoomMediaUploadResponse);
  rpc CompleteRoomMediaUpload(CompleteRoomMediaUploadRequest) returns (CompleteRoomMediaUploadResponse);
}

// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
service RoomQueryService {
  rpc AdminListRooms(AdminListRoomsRequest) returns (AdminListRoomsResponse);
  rpc AdminGetRoom(AdminGetRoomRequest) returns (AdminGetRoomResponse);
  rpc AdminGetRoomRocketConfig(AdminGetRoomRocketConfigRequest) returns (AdminGetRoomRocketConfigResponse);
  rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse);
  rpc AdminGetHumanRoomRobotConfig(AdminGetHumanRoomRobotConfigRequest) returns (AdminGetHumanRoomRobotConfigResponse);
  rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse);
  rpc AdminListRobotRooms(AdminListRobotRoomsRequest) returns (AdminListRobotRoomsResponse);
  rpc AdminFilterAvailableRoomRobots(AdminFilterAvailableRoomRobotsRequest) returns (AdminFilterAvailableRoomRobotsResponse);
  rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
  rpc ListRoomsByOwners(ListRoomsByOwnersRequest) returns (ListRoomsResponse);
  rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
  rpc ListRoomGiftLeaderboard(ListRoomGiftLeaderboardRequest) returns (ListRoomGiftLeaderboardResponse);
  rpc ListRoomContributionRank(ListRoomContributionRankRequest) returns (ListRoomContributionRankResponse);
  rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
  rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
  rpc BatchGetUserVoiceRoomPresences(BatchGetUserVoiceRoomPresencesRequest) returns (BatchGetUserVoiceRoomPresencesResponse);
  rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
  rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
  rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse);
  rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
  rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse);
}

// SendGiftLuckyHit 是 V2 微批中需要逐次播放的精简中奖命中。
// 声明追加在文件尾以保持既有 protobuf message index 稳定；字段可在前面的消息中正常引用。
message SendGiftLuckyHit {
  int32 gift_index = 1;
  string draw_id = 2;
  int64 target_user_id = 3;
  string selected_tier_id = 4;
  int64 multiplier_ppm = 5;
  int64 reward_coins = 6;
}

// SendGiftResultV2 是可持久化、可原样重放的送礼结算结果。
// 旧客户端继续读取 SendGiftResponse 顶层 V1 字段；V2 客户端只以该对象判断幂等、
// 提交顺序和实际结算数量，避免网络重试时拿“当前房间快照”冒充原始结算结果。
message SendGiftResultV2 {
  int32 api_version = 1;
  string command_id = 2;
  string combo_session_id = 3;
  int64 batch_seq = 4;
  bool idempotent_replay = 5;
  int64 committed_room_version = 6;
  string billing_receipt_id = 7;
  int64 coin_balance_after = 8;
  int64 gift_income_balance_after = 9;
  int32 settled_gift_count = 10;
  repeated int64 settled_target_user_ids = 11;
  // balance_version 是 sender 被扣费资产在该批次完成后的 wallet 账户版本；幂等重放保持首次值。
  int64 balance_version = 12;
  LuckyGiftDrawResult lucky_gift = 13;
  repeated LuckyGiftDrawResult lucky_gifts = 14;
  repeated SendGiftLuckyHit lucky_hits = 15;
  SendGiftBatchDisplay batch_display = 16;
  int64 room_heat = 17;
  repeated RankItem gift_rank = 18;
  // operation_status 当前成功响应固定为 committed；该字段让 V2 契约可以明确区分未来的异步受理态。
  string operation_status = 19;
}
