299 рядки
7.6 KiB
Go
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
|
|
}
|