package model import ( "crypto/sha256" "errors" "fmt" "slices" "github.com/QuantumNous/new-api/common" "gorm.io/gorm" ) // Keep priorities lossless in the web console's JSON number representation. const maxSortablePriority int64 = 1<<53 - 1 var ErrChannelPriorityConflict = errors.New("Channel priorities changed. Refresh and try again.") var ErrChannelPriorityInvalid = errors.New("Invalid channel priority move") var ErrChannelPriorityRange = errors.New("Channel priority is outside the sortable range") type ChannelPriority struct { ID int `json:"id"` Priority *int64 `json:"priority"` } func (item ChannelPriority) value() int64 { if item.Priority == nil { return 0 } return *item.Priority } type ChannelPriorityChange struct { ID int `json:"id"` Previous *int64 `json:"previous"` Priority *int64 `json:"priority"` } type ChannelPriorityOrder struct { Revision string `json:"revision"` Items []ChannelPriority `json:"items"` Changes []ChannelPriorityChange `json:"changes"` } type ChannelPriorityMove struct { Revision string `json:"revision"` ID int `json:"id"` TargetID int `json:"target_id"` Position string `json:"position"` // before / after use descending priority order Preview bool `json:"preview"` Restore []ChannelPriorityChange `json:"restore,omitempty"` } func channelPriorityRevision(items []ChannelPriority) string { data, _ := common.Marshal(items) return fmt.Sprintf("%x", sha256.Sum256(data)) } func GetChannelPriorityOrder() (ChannelPriorityOrder, error) { result := ChannelPriorityOrder{Items: []ChannelPriority{}, Changes: []ChannelPriorityChange{}} err := DB.Model(&Channel{}).Select("id", "priority").Order("id").Find(&result.Items).Error result.Revision = channelPriorityRevision(result.Items) return result, err } // ReorderChannelPriority updates only priorities, including the routing copy in // abilities. A snapshot check also protects preview/commit and undo from stale // clients; row locks serialize concurrent reorders on MySQL and PostgreSQL. func ReorderChannelPriority(move ChannelPriorityMove) (ChannelPriorityOrder, error) { result := ChannelPriorityOrder{Items: []ChannelPriority{}, Changes: []ChannelPriorityChange{}} err := DB.Transaction(func(tx *gorm.DB) error { query := tx.Model(&Channel{}).Select("id", "priority").Order("id") if !move.Preview { query = lockForUpdate(query) } if err := query.Find(&result.Items).Error; err != nil { return err } if move.Revision == "" || move.Revision != channelPriorityRevision(result.Items) { return ErrChannelPriorityConflict } changes, err := planChannelPriorityMove(result.Items, move) if err != nil { return err } result.Changes = changes byID := make(map[int]*ChannelPriority, len(result.Items)) for i := range result.Items { byID[result.Items[i].ID] = &result.Items[i] } for _, change := range changes { byID[change.ID].Priority = change.Priority if move.Preview { continue } if err := tx.Model(&Channel{}).Where("id = ?", change.ID).Update("priority", change.Priority).Error; err != nil { return err } if err := tx.Model(&Ability{}).Where("channel_id = ?", change.ID).Update("priority", change.Priority).Error; err != nil { return err } } result.Revision = channelPriorityRevision(result.Items) return nil }) return result, err } // Equal-priority channels remain together. Opening a slot shifts the shorter // contiguous run of occupied integer priorities, measured by affected channels. func planChannelPriorityMove(items []ChannelPriority, move ChannelPriorityMove) ([]ChannelPriorityChange, error) { changes := []ChannelPriorityChange{} byID := make(map[int]ChannelPriority, len(items)) for _, item := range items { if item.value() < -maxSortablePriority || item.value() > maxSortablePriority { return nil, ErrChannelPriorityRange } byID[item.ID] = item } if len(move.Restore) > 0 { if move.ID != 0 || move.TargetID != 0 || move.Position != "" { return nil, ErrChannelPriorityInvalid } seen := make(map[int]bool, len(move.Restore)) for _, change := range move.Restore { item, ok := byID[change.ID] if !ok || seen[change.ID] || (change.Priority != nil && (*change.Priority < -maxSortablePriority || *change.Priority > maxSortablePriority)) { return nil, ErrChannelPriorityInvalid } if !equalPriority(item.Priority, change.Previous) { return nil, ErrChannelPriorityConflict } seen[change.ID] = true if !equalPriority(item.Priority, change.Priority) { changes = append(changes, change) } } return changes, nil } item, ok := byID[move.ID] target, targetOK := byID[move.TargetID] if !ok || !targetOK || move.ID == move.TargetID || !slices.Contains([]string{"before", "after", "same"}, move.Position) { return nil, ErrChannelPriorityInvalid } values := make(map[int]int64, len(items)) for _, entry := range items { values[entry.ID] = entry.value() } if move.Position == "same" { values[item.ID] = target.value() } else { counts := make(map[int64]int) for _, entry := range items { if entry.ID != item.ID { counts[entry.value()]++ } } levels := make([]int64, 0, len(counts)) for level := range counts { levels = append(levels, level) } slices.SortFunc(levels, func(a, b int64) int { if a > b { return -1 } if a < b { return 1 } return 0 }) index := slices.Index(levels, target.value()) if move.Position == "after" { index++ } hi, lo := maxSortablePriority+1, -maxSortablePriority-1 if index > 0 { hi = levels[index-1] } if index < len(levels) { lo = levels[index] } // Dropping into the channel's existing slot must not renumber it. if item.value() < hi && item.value() > lo { return changes, nil } if hi-lo < 2 { up, down := index-1, index upCost, downCost := 0, 0 for up >= 0 { upCost += counts[levels[up]] if (up == 0 && levels[up] < maxSortablePriority) || (up > 0 && levels[up-1]-levels[up] > 1) { break } up-- } for down < len(levels) { downCost += counts[levels[down]] if (down == len(levels)-1 && levels[down] > -maxSortablePriority) || (down+1 < len(levels) && levels[down]-levels[down+1] > 1) { break } down++ } canUp, canDown := up >= 0 && index > 0, down < len(levels) && index < len(levels) if !canUp && !canDown { return nil, ErrChannelPriorityRange } if canUp && (!canDown || upCost < downCost) { for id, value := range values { if id != item.ID && value >= levels[index-1] && value <= levels[up] { values[id]++ } } hi++ } else { for id, value := range values { if id != item.ID && value <= levels[index] && value >= levels[down] { values[id]-- } } lo-- } } value := lo + (hi-lo)/2 if index == 0 { value = min(maxSortablePriority, lo+10) } else if index == len(levels) { value = max(-maxSortablePriority, hi-10) } values[item.ID] = value } for _, entry := range items { value := values[entry.ID] if value != entry.value() { changes = append(changes, ChannelPriorityChange{ID: entry.ID, Previous: entry.Priority, Priority: &value}) } } return changes, nil } func equalPriority(a, b *int64) bool { if a == nil || b == nil { return a == nil && b == nil } return *a == *b }