Файли
2026-11-08 03:05:46 +00:00

299 рядки
7.6 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"git.entityseven.com/rilaxik/pllist/internal/domain"
)
type ShareRepository struct {
db *gorm.DB
}
func NewShareRepository(db *gorm.DB) *ShareRepository {
return &ShareRepository{db: db}
}
type activeShareRow struct {
ID int64
UserID int64
TelegramID int64
Username *string
Role string
}
func (r *ShareRepository) ListByList(ctx context.Context, listID int64) ([]domain.Share, error) {
var active []activeShareRow
err := r.db.WithContext(ctx).
Table("list_shares").
Select("list_shares.id, list_shares.user_id, users.telegram_id, bot_access.username, list_shares.role").
Joins("JOIN users ON users.id = list_shares.user_id").
Joins("LEFT JOIN bot_access ON bot_access.telegram_id = users.telegram_id").
Where("list_shares.list_id = ?", listID).
Order("list_shares.created_at ASC").
Scan(&active).
Error
if err != nil {
return nil, fmt.Errorf("list shares: %w", err)
}
var pending []PendingShareModel
err = r.db.WithContext(ctx).
Where("list_id = ?", listID).
Order("created_at ASC").
Find(&pending).
Error
if err != nil {
return nil, fmt.Errorf("list pending shares: %w", err)
}
shares := make([]domain.Share, 0, len(active)+len(pending))
for _, row := range active {
share := domain.Share{
ID: row.ID,
ListID: listID,
UserID: row.UserID,
TelegramID: row.TelegramID,
Role: domain.Role(row.Role),
}
if row.Username != nil {
share.Username = *row.Username
}
shares = append(shares, share)
}
for _, row := range pending {
share := domain.Share{
ID: row.ID,
ListID: listID,
Role: domain.Role(row.Role),
Pending: true,
}
if row.TelegramID != nil {
share.TelegramID = *row.TelegramID
}
if row.Username != nil {
share.Username = *row.Username
}
shares = append(shares, share)
}
return shares, nil
}
func (r *ShareRepository) UpsertUser(
ctx context.Context, listID, userID int64, role domain.Role,
) error {
model := ListShareModel{ListID: listID, UserID: userID, Role: string(role)}
err := r.db.WithContext(ctx).
Clauses(userConflict()).
Create(&model).
Error
if err != nil {
return fmt.Errorf("upsert list share: %w", err)
}
return nil
}
func (r *ShareRepository) UpsertPendingTelegramID(
ctx context.Context, listID, createdByID, telegramID int64, role domain.Role,
) error {
model := PendingShareModel{
ListID: listID,
TelegramID: &telegramID,
Role: string(role),
CreatedByID: createdByID,
}
err := r.db.WithContext(ctx).
Clauses(pendingConflict("telegram_id")).
Create(&model).
Error
if err != nil {
return fmt.Errorf("upsert pending share by id: %w", err)
}
return nil
}
func (r *ShareRepository) UpsertPendingUsername(
ctx context.Context, listID, createdByID int64, username string, role domain.Role,
) error {
model := PendingShareModel{
ListID: listID,
Username: &username,
Role: string(role),
CreatedByID: createdByID,
}
err := r.db.WithContext(ctx).
Clauses(pendingConflict("username")).
Create(&model).
Error
if err != nil {
return fmt.Errorf("upsert pending share by username: %w", err)
}
return nil
}
func userConflict() clause.OnConflict {
return clause.OnConflict{
Columns: []clause.Column{{Name: "list_id"}, {Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{"role", "updated_at"}),
}
}
func pendingConflict(target string) clause.OnConflict {
return clause.OnConflict{
Columns: []clause.Column{{Name: "list_id"}, {Name: target}},
DoUpdates: clause.AssignmentColumns([]string{"role", "updated_at"}),
}
}
func (r *ShareRepository) SetUserRole(
ctx context.Context, listID, shareID int64, role domain.Role,
) error {
return r.setRole(ctx, &ListShareModel{}, shareID, listID, role)
}
func (r *ShareRepository) SetPendingRole(
ctx context.Context, listID, shareID int64, role domain.Role,
) error {
return r.setRole(ctx, &PendingShareModel{}, shareID, listID, role)
}
func (r *ShareRepository) setRole(
ctx context.Context, model any, shareID, listID int64, role domain.Role,
) error {
result := r.db.WithContext(ctx).
Model(model).
Where("id = ? AND list_id = ?", shareID, listID).
Update("role", string(role))
if result.Error != nil {
return fmt.Errorf("update share role: %w", result.Error)
}
if result.RowsAffected == 0 {
return domain.ErrNotFound
}
return nil
}
func (r *ShareRepository) RevokeUser(ctx context.Context, listID, shareID int64) error {
return r.revoke(ctx, &ListShareModel{}, listID, shareID)
}
func (r *ShareRepository) RevokePending(ctx context.Context, listID, shareID int64) error {
return r.revoke(ctx, &PendingShareModel{}, listID, shareID)
}
func (r *ShareRepository) revoke(
ctx context.Context, model any, listID, shareID int64,
) error {
result := r.db.WithContext(ctx).
Where("id = ? AND list_id = ?", shareID, listID).
Delete(model)
if result.Error != nil {
return fmt.Errorf("revoke share: %w", result.Error)
}
if result.RowsAffected == 0 {
return domain.ErrNotFound
}
return nil
}
// ResolvePending moves shares that match the user's Telegram identity into
// list_shares. It runs after the user row is ensured on every allowed update.
func (r *ShareRepository) ResolvePending(
ctx context.Context, userID, telegramID int64, username string,
) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
query := tx.Where("telegram_id = ?", telegramID)
if username != "" {
query = query.Or("username = ?", username)
}
var pending []PendingShareModel
if err := query.Find(&pending).Error; err != nil {
return err
}
if len(pending) == 0 {
return nil
}
ids := make([]int64, len(pending))
for i, row := range pending {
ids[i] = row.ID
share := ListShareModel{
ListID: row.ListID,
UserID: userID,
Role: row.Role,
}
if err := tx.Clauses(userConflict()).Create(&share).Error; err != nil {
return err
}
}
return tx.Where("id IN ?", ids).Delete(&PendingShareModel{}).Error
})
if err != nil {
return fmt.Errorf("resolve pending shares: %w", err)
}
return nil
}
// RemoveSharedAccess removes every non-owner list grant for a kicked user.
// Owned lists are untouched because ownership lives on lists.owner_id only.
func (r *ShareRepository) RemoveSharedAccess(
ctx context.Context, telegramID int64, username string,
) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if telegramID > 0 {
var userIDs []int64
if err := tx.Model(&UserModel{}).
Where("telegram_id = ?", telegramID).
Pluck("id", &userIDs).
Error; err != nil {
return err
}
if len(userIDs) > 0 {
if err := tx.Where("user_id IN ?", userIDs).
Delete(&ListShareModel{}).
Error; err != nil {
return err
}
}
if err := tx.Where("telegram_id = ?", telegramID).
Delete(&PendingShareModel{}).
Error; err != nil {
return err
}
// A pending share may have been stored by username before the
// grant was bound to this Telegram ID.
var grant BotAccessModel
err := tx.Where("telegram_id = ?", telegramID).First(&grant).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if err == nil && grant.Username != nil && *grant.Username != "" {
if err := tx.Where("username = ?", *grant.Username).
Delete(&PendingShareModel{}).
Error; err != nil {
return err
}
}
}
if username != "" {
if err := tx.Where("username = ?", username).
Delete(&PendingShareModel{}).
Error; err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("remove shared list access: %w", err)
}
return nil
}