chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,465 @@
package memory
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"telesrv/internal/domain"
)
// GroupCallStore 是 store.GroupCallStore 的进程内实现(rpc 单测 fixture 用)。
// 行为契约与 postgres 实现由共享 contract test 钉死(groupcall contract test),
// 凡动 version/参与者语义两边必须同步并跑 PG 集成。
type overrideKey struct {
callID, setter, target int64
}
type GroupCallStore struct {
mu sync.Mutex
calls map[int64]domain.GroupCall
activeByChan map[int64]int64 // channelID → active callID
participants map[int64]map[int64]domain.GroupCallParticipant // callID → userID → row
overrides map[overrideKey]domain.GroupCallParticipantOverride
raiseHandSeq map[int64]int64 // callID → 单调举手序号
nextSyntheticID int64
}
// NewGroupCallStore 创建内存实现。
func NewGroupCallStore() *GroupCallStore {
return &GroupCallStore{
calls: make(map[int64]domain.GroupCall),
activeByChan: make(map[int64]int64),
participants: make(map[int64]map[int64]domain.GroupCallParticipant),
overrides: make(map[overrideKey]domain.GroupCallParticipantOverride),
raiseHandSeq: make(map[int64]int64),
}
}
func (s *GroupCallStore) CreateGroupCall(_ context.Context, call domain.GroupCall) (domain.GroupCall, error) {
if call.ID == 0 || call.ChannelID == 0 || call.AccessHash == 0 {
return domain.GroupCall{}, domain.ErrGroupCallInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if activeID, ok := s.activeByChan[call.ChannelID]; ok {
if existing, ok := s.calls[activeID]; ok && existing.Active() {
return domain.GroupCall{}, domain.ErrGroupCallAlreadyStarted
}
}
if _, exists := s.calls[call.ID]; exists {
return domain.GroupCall{}, domain.ErrGroupCallInvalid
}
call.State = domain.GroupCallStateActive
if call.Version <= 0 {
call.Version = 1
}
call.ParticipantsCount = 0
s.calls[call.ID] = call
s.activeByChan[call.ChannelID] = call.ID
s.participants[call.ID] = make(map[int64]domain.GroupCallParticipant)
return call, nil
}
func (s *GroupCallStore) GetGroupCall(_ context.Context, callID int64) (domain.GroupCall, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
return call, ok, nil
}
func (s *GroupCallStore) JoinGroupCall(_ context.Context, req domain.JoinGroupCallRequest) (domain.GroupCallMutation, error) {
if req.SSRC == 0 {
return domain.GroupCallMutation{}, domain.ErrGroupCallInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[req.CallID]
if !ok {
return domain.GroupCallMutation{}, domain.ErrGroupCallInvalid
}
if !call.Active() {
return domain.GroupCallMutation{}, domain.ErrGroupCallDiscarded
}
rows := s.participants[req.CallID]
for userID, p := range rows {
if !p.Left && p.SSRC == req.SSRC && userID != req.UserID {
return domain.GroupCallMutation{}, domain.ErrGroupCallSSRCDuplicate
}
}
existing, rejoining := rows[req.UserID]
wasActive := rejoining && !existing.Left
p := domain.GroupCallParticipant{
CallID: req.CallID,
UserID: req.UserID,
SSRC: req.SSRC,
JoinDate: req.Now,
ActiveDate: req.Now,
LastCheckDate: req.Now,
// VideoJSON 整体替换、PresentationJSON 随全新行清空(rejoin 后客户端
// 会重发 joinGroupCallPresentation,旧屏幕登记必须作废)。
VideoJSON: append([]byte(nil), req.VideoJSON...),
}
if rejoining && wasActive {
// 同设备换 ssrc 的 rejoin 保留原 join_date(列表排序稳定)。
p.JoinDate = existing.JoinDate
}
// join_muted 策略:普通成员入会即静音且不可自行开麦(muted_by_admin)。
if call.JoinMuted && !req.IsAdmin {
p.Muted = true
p.MutedByAdmin = true
}
rows[req.UserID] = p
if !wasActive {
call.ParticipantsCount++
}
call.Version++
s.calls[req.CallID] = call
return domain.GroupCallMutation{Call: call, Participant: p}, nil
}
func (s *GroupCallStore) LeaveGroupCall(_ context.Context, callID, userID int64, now int) (domain.GroupCallMutation, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCallMutation{}, domain.ErrGroupCallInvalid
}
p, ok := s.participants[callID][userID]
if !ok || p.Left {
return domain.GroupCallMutation{}, domain.ErrGroupCallNotJoined
}
p.Left = true
p.ActiveDate = now
s.participants[callID][userID] = p
if call.ParticipantsCount > 0 {
call.ParticipantsCount--
}
call.Version++
s.calls[callID] = call
return domain.GroupCallMutation{Call: call, Participant: p}, nil
}
func (s *GroupCallStore) DiscardGroupCall(_ context.Context, callID int64, now int) (domain.GroupCall, []domain.GroupCallParticipant, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCall{}, nil, domain.ErrGroupCallInvalid
}
if !call.Active() {
return domain.GroupCall{}, nil, domain.ErrGroupCallDiscarded
}
var active []domain.GroupCallParticipant
for userID, p := range s.participants[callID] {
if p.Left {
continue
}
active = append(active, p)
p.Left = true
p.ActiveDate = now
s.participants[callID][userID] = p
}
call.State = domain.GroupCallStateDiscarded
call.DiscardedAt = now
call.Duration = max(0, now-call.CreatedAt)
call.ParticipantsCount = 0
call.Version++
s.calls[callID] = call
if s.activeByChan[call.ChannelID] == callID {
delete(s.activeByChan, call.ChannelID)
}
sort.Slice(active, func(i, j int) bool { return active[i].UserID < active[j].UserID })
return call, active, nil
}
func (s *GroupCallStore) TouchParticipant(_ context.Context, callID, userID int64, now int) ([]int64, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return nil, false, domain.ErrGroupCallInvalid
}
if !call.Active() {
return nil, false, nil
}
p, ok := s.participants[callID][userID]
if !ok || p.Left {
return nil, false, nil
}
p.LastCheckDate = now
p.ActiveDate = now
s.participants[callID][userID] = p
return []int64{p.SSRC}, true, nil
}
func (s *GroupCallStore) GetParticipant(_ context.Context, callID, userID int64) (domain.GroupCallParticipant, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.participants[callID][userID]
return p, ok, nil
}
func (s *GroupCallStore) ListParticipants(_ context.Context, callID int64, offset string, limit int) (domain.GroupCallParticipantPage, error) {
if limit <= 0 || limit > 200 {
limit = 200
}
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCallParticipantPage{}, domain.ErrGroupCallInvalid
}
var rows []domain.GroupCallParticipant
for _, p := range s.participants[callID] {
if !p.Left {
rows = append(rows, p)
}
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].JoinDate != rows[j].JoinDate {
return rows[i].JoinDate < rows[j].JoinDate
}
return rows[i].UserID < rows[j].UserID
})
page := domain.GroupCallParticipantPage{Count: len(rows), Version: call.Version}
offDate, offUser, hasOffset := parseGroupCallOffset(offset)
for _, p := range rows {
if hasOffset && (p.JoinDate < offDate || (p.JoinDate == offDate && p.UserID <= offUser)) {
continue
}
page.Participants = append(page.Participants, p)
if len(page.Participants) == limit {
break
}
}
if n := len(page.Participants); n == limit && n < page.Count {
last := page.Participants[n-1]
page.NextOffset = formatGroupCallOffset(last.JoinDate, last.UserID)
}
return page, nil
}
func (s *GroupCallStore) UpdateParticipant(_ context.Context, callID, userID int64, update domain.GroupCallParticipantUpdate) (domain.GroupCallMutation, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallInvalid
}
if !call.Active() {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallDiscarded
}
p, ok := s.participants[callID][userID]
if !ok || p.Left {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallNotJoined
}
changed := applyGroupCallParticipantUpdate(&p, update)
if !changed {
return domain.GroupCallMutation{Call: call, Participant: p}, false, nil
}
if update.Now > 0 {
p.ActiveDate = update.Now
}
s.participants[callID][userID] = p
call.Version++
s.calls[callID] = call
return domain.GroupCallMutation{Call: call, Participant: p}, true, nil
}
func (s *GroupCallStore) SetGroupCallTitle(_ context.Context, callID int64, title string) (domain.GroupCall, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCall{}, false, domain.ErrGroupCallInvalid
}
if call.Title == title {
return call, false, nil
}
call.Title = title
s.calls[callID] = call
return call, true, nil
}
func (s *GroupCallStore) SetGroupCallJoinMuted(_ context.Context, callID int64, joinMuted bool) (domain.GroupCall, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.GroupCall{}, false, domain.ErrGroupCallInvalid
}
if call.JoinMuted == joinMuted {
return call, false, nil
}
call.JoinMuted = joinMuted
s.calls[callID] = call
return call, true, nil
}
func (s *GroupCallStore) SetStartedMessageID(_ context.Context, callID int64, msgID int) error {
s.mu.Lock()
defer s.mu.Unlock()
call, ok := s.calls[callID]
if !ok {
return domain.ErrGroupCallInvalid
}
call.StartedMsgID = msgID
s.calls[callID] = call
return nil
}
func (s *GroupCallStore) SweepStaleParticipants(_ context.Context, checkOlderThan, now, limit int) ([]domain.GroupCallMutation, error) {
if limit <= 0 {
limit = 100
}
s.mu.Lock()
defer s.mu.Unlock()
var out []domain.GroupCallMutation
for callID, rows := range s.participants {
call := s.calls[callID]
if !call.Active() {
continue
}
userIDs := make([]int64, 0, len(rows))
for userID := range rows {
userIDs = append(userIDs, userID)
}
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
for _, userID := range userIDs {
p := rows[userID]
if p.Left || p.LastCheckDate >= checkOlderThan {
continue
}
p.Left = true
p.ActiveDate = now
rows[userID] = p
if call.ParticipantsCount > 0 {
call.ParticipantsCount--
}
call.Version++
out = append(out, domain.GroupCallMutation{Call: call, Participant: p})
if len(out) == limit {
s.calls[callID] = call
return out, nil
}
}
s.calls[callID] = call
}
return out, nil
}
func (s *GroupCallStore) ResetAllParticipants(_ context.Context, now int) ([]domain.GroupCall, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []domain.GroupCall
for callID, rows := range s.participants {
call := s.calls[callID]
if !call.Active() {
continue
}
cleared := false
for userID, p := range rows {
if p.Left {
continue
}
p.Left = true
p.ActiveDate = now
rows[userID] = p
cleared = true
}
if !cleared {
continue
}
call.ParticipantsCount = 0
call.Version++
s.calls[callID] = call
out = append(out, call)
}
return out, nil
}
func applyGroupCallParticipantUpdate(p *domain.GroupCallParticipant, u domain.GroupCallParticipantUpdate) bool {
changed := false
if u.Muted != nil && p.Muted != *u.Muted {
p.Muted = *u.Muted
changed = true
}
if u.MutedByAdmin != nil && p.MutedByAdmin != *u.MutedByAdmin {
p.MutedByAdmin = *u.MutedByAdmin
changed = true
}
if u.VolumeByAdmin != nil && p.VolumeByAdmin != *u.VolumeByAdmin {
p.VolumeByAdmin = *u.VolumeByAdmin
changed = true
}
if u.RaiseHandRating != nil && p.RaiseHandRating != *u.RaiseHandRating {
p.RaiseHandRating = *u.RaiseHandRating
changed = true
}
if u.VideoJSON != nil && string(p.VideoJSON) != string(*u.VideoJSON) {
p.VideoJSON = append([]byte(nil), *u.VideoJSON...)
changed = true
}
if u.PresentationJSON != nil && string(p.PresentationJSON) != string(*u.PresentationJSON) {
p.PresentationJSON = append([]byte(nil), *u.PresentationJSON...)
changed = true
}
return changed
}
func parseGroupCallOffset(offset string) (joinDate int, userID int64, ok bool) {
if offset == "" {
return 0, 0, false
}
parts := strings.Split(offset, ":")
if len(parts) != 3 || parts[0] != "j" {
return 0, 0, false
}
d, err1 := strconv.Atoi(parts[1])
u, err2 := strconv.ParseInt(parts[2], 10, 64)
if err1 != nil || err2 != nil {
return 0, 0, false
}
return d, u, true
}
func formatGroupCallOffset(joinDate int, userID int64) string {
return fmt.Sprintf("j:%d:%d", joinDate, userID)
}
func (s *GroupCallStore) NextRaiseHandRating(_ context.Context, callID int64) (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.raiseHandSeq[callID]++
return s.raiseHandSeq[callID], nil
}
func (s *GroupCallStore) SetParticipantOverride(_ context.Context, callID, setterUserID, targetUserID int64, override domain.GroupCallParticipantOverride, clear bool) error {
s.mu.Lock()
defer s.mu.Unlock()
key := overrideKey{callID: callID, setter: setterUserID, target: targetUserID}
if clear {
delete(s.overrides, key)
return nil
}
s.overrides[key] = override
return nil
}
func (s *GroupCallStore) GetParticipantOverride(_ context.Context, callID, setterUserID, targetUserID int64) (domain.GroupCallParticipantOverride, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
ov, ok := s.overrides[overrideKey{callID: callID, setter: setterUserID, target: targetUserID}]
return ov, ok, nil
}
func max(a, b int) int {
if a > b {
return a
}
return b
}