owpengram-server/internal/rpc/phone_group_call_edit.go

330 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
// editGroupCallParticipant 权限矩阵M2 完整版):
//
// 操作 自己→自己 普通成员→他人 admin/creator→他人
// muted=true muted, can_self_unmute overrides.muted_by_you muted_by_admin禁言
// muted=false 仅当 can_self_unmute 清 overrides 允许发言(清 muted_by_admin+举手)
// volume 忽略 overrides.volume volume_by_admin全员
// raise_hand rating=全局单调序号 ✗ admin 放下他人手
//
// ⚠ P1-5self 的 video_stopped/video_paused 一律接受/忽略转发,绝不返回错误
// edit 失败在客户端有 rejoin 风暴风险)。
func (r *Router) onPhoneEditGroupCallParticipant(ctx context.Context, req *tg.PhoneEditGroupCallParticipantRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
scope, err := r.groupCallScopeFrom(ctx, req.Call)
if err != nil {
return nil, err
}
targetID, err := r.groupCallParticipantTarget(ctx, scope.userID, req.Participant)
if err != nil {
return nil, err
}
target, found, err := r.deps.GroupCalls.Participant(ctx, scope.call.ID, targetID)
if err != nil {
return nil, internalErr()
}
if !found || target.Left {
return nil, groupCallJoinMissingErr()
}
switch {
case targetID == scope.userID:
return r.editSelfParticipant(ctx, scope, req, target)
case scope.canManage():
return r.adminEditParticipant(ctx, scope, req, target)
default:
return r.memberOverrideParticipant(ctx, scope, req, targetID)
}
}
// editSelfParticipant 处理 self 维度mute 自由翻转、举手、视频开关/暂停)。
// ⚠ P1-5video/presentation 维度的 self-edit 绝不返回错误edit 失败有客户端
// rejoin 风暴风险;幂等重放也回成功空 Updates 而非 GROUPCALL_NOT_MODIFIED
func (r *Router) editSelfParticipant(ctx context.Context, scope *groupCallScope, req *tg.PhoneEditGroupCallParticipantRequest, self domain.GroupCallParticipant) (tg.UpdatesClass, error) {
update := domain.GroupCallParticipantUpdate{Now: int(r.clock.Now().Unix())}
if raise, ok := req.GetRaiseHand(); ok {
if raise {
rating, err := r.deps.GroupCalls.NextRaiseHandRating(ctx, scope.call.ID)
if err != nil {
return nil, internalErr()
}
update.RaiseHandRating = &rating
} else {
zero := int64(0)
update.RaiseHandRating = &zero
}
}
if muted, ok := req.GetMuted(); ok {
if self.MutedByAdmin && !muted {
// 被管理员压制时不能自行 unmute客户端此时只会发 raise_hand
return nil, groupCallForbiddenErr()
}
update.Muted = &muted
}
videoEdit := false
// video_stopped翻转 video 字段的出现/消失。endpoint 与 source_groups 来自
// join 时的存档(开关摄像头不重发 join缺档极老会话静默容忍。
if stopped, ok := req.GetVideoStopped(); ok {
videoEdit = true
if st, found := decodeVideoState(self.VideoJSON); found {
st.Active = !stopped && len(st.SourceGroups) > 0
if !st.Active {
st.Paused = false
}
raw := encodeVideoState(st)
update.VideoJSON = &raw
}
}
// video_pausedvideo 字段保留、置 paused观看端渲染占位
if paused, ok := req.GetVideoPaused(); ok {
videoEdit = true
if st, found := decodeVideoState(self.VideoJSON); found && st.Active {
st.Paused = paused
raw := encodeVideoState(st)
update.VideoJSON = &raw
}
}
if paused, ok := req.GetPresentationPaused(); ok {
videoEdit = true
if st, found := decodeVideoState(self.PresentationJSON); found && st.Active {
st.Paused = paused
raw := encodeVideoState(st)
update.PresentationJSON = &raw
}
}
mut, changed, err := r.deps.GroupCalls.UpdateParticipant(ctx, scope.call.ID, scope.userID, update)
if err != nil {
if videoEdit {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
return nil, groupCallErr(err)
}
if !changed {
if videoEdit {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
return nil, groupCallNotModifiedErr()
}
channel := r.groupCallMutationFanout(ctx, scope.channel, mut)
return r.groupCallUpdateContainer(ctx, scope.userID, channel,
&tg.UpdateGroupCallParticipants{
Call: &tg.InputGroupCall{ID: mut.Call.ID, AccessHash: mut.Call.AccessHash},
Participants: tgGroupCallParticipants([]domain.GroupCallParticipant{mut.Participant}, scope.userID),
Version: mut.Call.Version,
}, []int64{scope.userID}), nil
}
// adminEditParticipant 处理 admin→他人禁言/允许发言/全局音量/放下举手)。
func (r *Router) adminEditParticipant(ctx context.Context, scope *groupCallScope, req *tg.PhoneEditGroupCallParticipantRequest, target domain.GroupCallParticipant) (tg.UpdatesClass, error) {
update := domain.GroupCallParticipantUpdate{Now: int(r.clock.Now().Unix())}
if muted, ok := req.GetMuted(); ok {
mutedByAdmin := muted
update.MutedByAdmin = &mutedByAdmin
if muted {
// 禁言muted=true 且 can_self_unmute=false。
update.Muted = &muted
} else {
// "允许发言":只恢复 can_self_unmute 并清举手,不替用户开麦
//(官方语义:用户收到后仍是 muted需自行 unmute
zero := int64(0)
update.RaiseHandRating = &zero
}
}
if raise, ok := req.GetRaiseHand(); ok && !raise {
// admin 放下他人举手。
zero := int64(0)
update.RaiseHandRating = &zero
}
if volume, ok := req.GetVolume(); ok {
update.VolumeByAdmin = &volume
}
return r.applyParticipantMutation(ctx, scope, target.UserID, update)
}
// memberOverrideParticipant 处理普通成员→他人(本地静音/本地音量per-viewer
// 仅 setter 自己可见,不进全房间 version
func (r *Router) memberOverrideParticipant(ctx context.Context, scope *groupCallScope, req *tg.PhoneEditGroupCallParticipantRequest, targetID int64) (tg.UpdatesClass, error) {
ov, _, err := r.deps.GroupCalls.ParticipantOverride(ctx, scope.call.ID, scope.userID, targetID)
if err != nil {
return nil, internalErr()
}
changed := false
if muted, ok := req.GetMuted(); ok {
if ov.MutedByYou != muted {
ov.MutedByYou = muted
changed = true
}
}
if volume, ok := req.GetVolume(); ok {
if ov.Volume != volume {
ov.Volume = volume
changed = true
}
}
if !changed {
return nil, groupCallNotModifiedErr()
}
clear := !ov.MutedByYou && ov.Volume == 0
if err := r.deps.GroupCalls.SetParticipantOverride(ctx, scope.call.ID, scope.userID, targetID, ov, clear); err != nil {
return nil, internalErr()
}
// per-viewer只推给 setter 自己(其全部设备,带 min flag 防覆盖本地状态)。
target, found, err := r.deps.GroupCalls.Participant(ctx, scope.call.ID, targetID)
if err != nil || !found {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
row := applyParticipantOverride(tgGroupCallParticipant(target, scope.userID), ov)
update := &tg.UpdateGroupCallParticipants{
Call: &tg.InputGroupCall{ID: scope.call.ID, AccessHash: scope.call.AccessHash},
Participants: []tg.GroupCallParticipant{row},
Version: scope.call.Version, // overrides 不推进 version
}
out := r.groupCallUpdateContainer(ctx, scope.userID, scope.channel, update, []int64{targetID})
r.pushUserMessage(ctx, scope.userID, "group call override", out)
return out, nil
}
// applyParticipantMutation 把全房间维度的字段级更新落库并扇出version++)。
func (r *Router) applyParticipantMutation(ctx context.Context, scope *groupCallScope, targetID int64, update domain.GroupCallParticipantUpdate) (tg.UpdatesClass, error) {
mut, changed, err := r.deps.GroupCalls.UpdateParticipant(ctx, scope.call.ID, targetID, update)
if err != nil {
return nil, groupCallErr(err)
}
if !changed {
return nil, groupCallNotModifiedErr()
}
channel := r.groupCallMutationFanout(ctx, scope.channel, mut)
return r.groupCallUpdateContainer(ctx, scope.userID, channel,
&tg.UpdateGroupCallParticipants{
Call: &tg.InputGroupCall{ID: mut.Call.ID, AccessHash: mut.Call.AccessHash},
Participants: tgGroupCallParticipants([]domain.GroupCallParticipant{mut.Participant}, scope.userID),
Version: mut.Call.Version,
}, []int64{targetID}), nil
}
func (r *Router) onPhoneEditGroupCallTitle(ctx context.Context, req *tg.PhoneEditGroupCallTitleRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
scope, err := r.groupCallScopeFrom(ctx, req.Call)
if err != nil {
return nil, err
}
if !scope.canManage() {
return nil, tgerr400("CHAT_ADMIN_REQUIRED")
}
call, changed, err := r.deps.GroupCalls.SetTitle(ctx, scope.call.ID, req.Title)
if err != nil {
return nil, groupCallErr(err)
}
if !changed {
return nil, groupCallNotModifiedErr()
}
r.pushGroupCallUpdate(ctx, scope.channel, call)
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
groupCallUpdateFor(scope.channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
}
func (r *Router) onPhoneToggleGroupCallSettings(ctx context.Context, req *tg.PhoneToggleGroupCallSettingsRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
scope, err := r.groupCallScopeFrom(ctx, req.Call)
if err != nil {
return nil, err
}
if !scope.canManage() {
return nil, tgerr400("CHAT_ADMIN_REQUIRED")
}
joinMuted, ok := req.GetJoinMuted()
if !ok {
// reset_invite_hash / messages_enabled 等范围外开关:无变化语义。
return nil, groupCallNotModifiedErr()
}
call, changed, err := r.deps.GroupCalls.SetJoinMuted(ctx, scope.call.ID, joinMuted)
if err != nil {
return nil, groupCallErr(err)
}
if !changed {
return nil, groupCallNotModifiedErr()
}
r.pushGroupCallUpdate(ctx, scope.channel, call)
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
groupCallUpdateFor(scope.channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
}
const maxInviteToGroupCallUsers = 10
// onPhoneInviteToGroupCall 发 messageActionInviteToGroupCall 服务消息(带频道 pts
// 被邀请者通过正常频道消息收到可点入会卡片;不直接拉人入会。
func (r *Router) onPhoneInviteToGroupCall(ctx context.Context, req *tg.PhoneInviteToGroupCallRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
scope, err := r.groupCallScopeFrom(ctx, req.Call)
if err != nil {
return nil, err
}
if scope.call.Conference() {
return nil, notImplementedErr()
}
if len(req.Users) == 0 || len(req.Users) > maxInviteToGroupCallUsers {
return nil, limitInvalidErr()
}
targetIDs := make([]int64, 0, len(req.Users))
for _, in := range req.Users {
u, found, err := r.userFromInput(ctx, scope.userID, in)
if err != nil {
return nil, internalErr()
}
if !found || u.ID == 0 {
return nil, userIDInvalidErr()
}
// 目标必须是本群成员。
member, err := r.deps.Channels.GetParticipant(ctx, scope.userID, scope.channel.ID, u.ID)
if err != nil || member.Status != domain.ChannelMemberActive {
return nil, tgerr400("USER_NOT_PARTICIPANT")
}
targetIDs = append(targetIDs, u.ID)
}
now := int(r.clock.Now().Unix())
res, err := r.deps.Channels.AppendCallServiceMessage(ctx, scope.channel.ID, scope.userID, now, domain.ChannelMessageAction{
Type: domain.ChannelActionInviteToGroupCall,
CallID: scope.call.ID,
CallAccessHash: scope.call.AccessHash,
UserIDs: targetIDs,
})
if err != nil {
return nil, internalErr()
}
r.pushGroupCallServiceMessage(ctx, scope.userID, res)
out := r.groupCallUpdateContainer(ctx, scope.userID, res.Channel, nil, targetIDs)
out.Updates = nil
if msgUpdate := tgChannelUpdate(scope.userID, res.Event); msgUpdate != nil {
out.Updates = append(out.Updates, msgUpdate)
}
return out, nil
}
// groupCallParticipantTarget 解析 editGroupCallParticipant 的目标(仅 user peer
func (r *Router) groupCallParticipantTarget(ctx context.Context, currentUserID int64, peer tg.InputPeerClass) (int64, error) {
switch v := peer.(type) {
case *tg.InputPeerSelf:
return currentUserID, nil
case *tg.InputPeerUser:
return v.UserID, nil
default:
return 0, peerIDInvalidErr()
}
}