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

143 рядки
4.1 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type BotAccessRepository struct {
db *gorm.DB
}
func NewBotAccessRepository(db *gorm.DB) *BotAccessRepository {
return &BotAccessRepository{db: db}
}
func (r *BotAccessRepository) AllowTelegramID(ctx context.Context, telegramID int64) (bool, error) {
model := BotAccessModel{TelegramID: &telegramID}
result := r.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "telegram_id"}},
DoNothing: true,
}).
Create(&model)
if result.Error != nil {
return false, fmt.Errorf("allow telegram id: %w", result.Error)
}
return result.RowsAffected > 0, nil
}
func (r *BotAccessRepository) AllowUsername(ctx context.Context, username string) (bool, error) {
model := BotAccessModel{Username: &username}
result := r.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "username"}},
DoNothing: true,
}).
Create(&model)
if result.Error != nil {
return false, fmt.Errorf("allow username: %w", result.Error)
}
return result.RowsAffected > 0, nil
}
// TelegramIDForUsername returns the bound Telegram ID for a username grant.
// found is true for a pending grant, where telegramID is zero.
func (r *BotAccessRepository) TelegramIDForUsername(
ctx context.Context, username string,
) (int64, bool, error) {
var model BotAccessModel
err := r.db.WithContext(ctx).Where("username = ?", username).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, false, nil
}
if err != nil {
return 0, false, fmt.Errorf("get bot access by username: %w", err)
}
if model.TelegramID == nil {
return 0, true, nil
}
return *model.TelegramID, true, nil
}
func (r *BotAccessRepository) IsAllowed(ctx context.Context, telegramID int64) (bool, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&BotAccessModel{}).
Where("telegram_id = ?", telegramID).
Count(&count).Error
if err != nil {
return false, fmt.Errorf("check bot access: %w", err)
}
return count > 0, nil
}
// Claim binds a pending username grant to the user who just contacted the bot.
// If the user already has an ID grant, a duplicate pending row is removed and
// the stored username is refreshed so /kick can use the current handle.
func (r *BotAccessRepository) Claim(ctx context.Context, telegramID int64, username string) error {
if username == "" {
return nil
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var byID BotAccessModel
err := tx.Where("telegram_id = ?", telegramID).First(&byID).Error
if err == nil {
if err := tx.Where("username = ? AND id <> ?", username, byID.ID).
Delete(&BotAccessModel{}).Error; err != nil {
return err
}
if byID.Username != nil && *byID.Username == username {
return nil
}
return tx.Model(&byID).Update("username", username).Error
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
var pending BotAccessModel
err = tx.Where("username = ?", username).First(&pending).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
// A username can be reassigned. Do not transfer a grant that already
// belongs to another Telegram ID.
if pending.TelegramID != nil && *pending.TelegramID != telegramID {
return nil
}
return tx.Model(&pending).Update("telegram_id", telegramID).Error
})
if err != nil {
return fmt.Errorf("claim bot access: %w", err)
}
return nil
}
func (r *BotAccessRepository) RevokeTelegramID(ctx context.Context, telegramID int64) (bool, error) {
result := r.db.WithContext(ctx).
Where("telegram_id = ?", telegramID).
Delete(&BotAccessModel{})
if result.Error != nil {
return false, fmt.Errorf("revoke telegram id: %w", result.Error)
}
return result.RowsAffected > 0, nil
}
func (r *BotAccessRepository) RevokeUsername(ctx context.Context, username string) (bool, error) {
result := r.db.WithContext(ctx).
Where("username = ?", username).
Delete(&BotAccessModel{})
if result.Error != nil {
return false, fmt.Errorf("revoke username: %w", result.Error)
}
return result.RowsAffected > 0, nil
}