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

258 рядки
7.0 KiB
Go

package service
import (
"context"
"fmt"
"git.entityseven.com/rilaxik/pllist/internal/domain"
)
type ShareRepository interface {
ListByList(ctx context.Context, listID int64) ([]domain.Share, error)
UpsertUser(ctx context.Context, listID, userID int64, role domain.Role) error
UpsertPendingTelegramID(
ctx context.Context, listID, createdByID, telegramID int64, role domain.Role,
) error
UpsertPendingUsername(
ctx context.Context, listID, createdByID int64, username string, role domain.Role,
) error
SetUserRole(ctx context.Context, listID, shareID int64, role domain.Role) error
SetPendingRole(ctx context.Context, listID, shareID int64, role domain.Role) error
RevokeUser(ctx context.Context, listID, shareID int64) error
RevokePending(ctx context.Context, listID, shareID int64) error
ResolvePending(ctx context.Context, userID, telegramID int64, username string) error
RemoveSharedAccess(ctx context.Context, telegramID int64, username string) error
}
type UserLookup interface {
GetByTelegramID(ctx context.Context, telegramID int64) (domain.User, bool, error)
}
type ShareService struct {
shares ShareRepository
users UserLookup
access *AccessService
botAccess *BotAccessService
}
func NewShareService(
shares ShareRepository,
users UserLookup,
access *AccessService,
botAccess *BotAccessService,
) *ShareService {
return &ShareService{
shares: shares,
users: users,
access: access,
botAccess: botAccess,
}
}
type ShareList struct {
Access domain.Access
Shares []domain.Share
}
func (s *ShareService) List(
ctx context.Context, userID, listID int64,
) (ShareList, error) {
access, err := s.access.Authorize(ctx, userID, listID, domain.ActionManageShares)
if err != nil {
return ShareList{}, err
}
shares, err := s.shares.ListByList(ctx, listID)
if err != nil {
return ShareList{}, err
}
return ShareList{Access: access, Shares: shares}, nil
}
// Find loads the share list and one share so detail views re-check permissions
// against fresh data.
func (s *ShareService) Find(
ctx context.Context, userID, listID, shareID int64, pending bool,
) (ShareList, domain.Share, error) {
list, err := s.List(ctx, userID, listID)
if err != nil {
return ShareList{}, domain.Share{}, err
}
for _, share := range list.Shares {
if share.ID == shareID && share.Pending == pending {
return list, share, nil
}
}
return ShareList{}, domain.Share{}, domain.ErrNotFound
}
// Prepare validates a share target and returns the label for the role prompt.
func (s *ShareService) Prepare(
ctx context.Context, owner domain.User, listID int64, raw string,
) (string, error) {
target, err := s.resolveTarget(ctx, owner, listID, raw)
if err != nil {
return "", err
}
return target.label(), nil
}
func (s *ShareService) Share(
ctx context.Context, owner domain.User, listID int64, raw string, role domain.Role,
) error {
if !validShareRole(role) {
return domain.NewValidationError("Choose reader or editor access.")
}
target, err := s.resolveTarget(ctx, owner, listID, raw)
if err != nil {
return err
}
switch {
case target.userID > 0:
return s.shares.UpsertUser(ctx, listID, target.userID, role)
case target.telegramID > 0:
return s.shares.UpsertPendingTelegramID(
ctx, listID, owner.ID, target.telegramID, role,
)
default:
return s.shares.UpsertPendingUsername(ctx, listID, owner.ID, target.username, role)
}
}
func (s *ShareService) SetRole(
ctx context.Context, ownerID, listID, shareID int64, pending bool, role domain.Role,
) error {
if !validShareRole(role) {
return domain.NewValidationError("Choose reader or editor access.")
}
if _, err := s.access.Authorize(ctx, ownerID, listID, domain.ActionManageShares); err != nil {
return err
}
if pending {
return s.shares.SetPendingRole(ctx, listID, shareID, role)
}
return s.shares.SetUserRole(ctx, listID, shareID, role)
}
func (s *ShareService) Revoke(
ctx context.Context, ownerID, listID, shareID int64, pending bool,
) error {
if _, err := s.access.Authorize(ctx, ownerID, listID, domain.ActionManageShares); err != nil {
return err
}
if pending {
return s.shares.RevokePending(ctx, listID, shareID)
}
return s.shares.RevokeUser(ctx, listID, shareID)
}
// ResolvePending activates pending shares when the target contacts the bot.
func (s *ShareService) ResolvePending(
ctx context.Context, userID, telegramID int64, username string,
) error {
return s.shares.ResolvePending(ctx, userID, telegramID, NormalizeUsername(username))
}
type resolvedShareTarget struct {
userID int64
telegramID int64
username string
}
func (t resolvedShareTarget) label() string {
if t.username != "" {
return "@" + t.username
}
return fmt.Sprintf("ID %d", t.telegramID)
}
func (s *ShareService) resolveTarget(
ctx context.Context, owner domain.User, listID int64, raw string,
) (resolvedShareTarget, error) {
access, err := s.access.Authorize(ctx, owner.ID, listID, domain.ActionManageShares)
if err != nil {
return resolvedShareTarget{}, err
}
target, err := parseGrantTarget(raw, "Send a Telegram @username or numeric user ID.")
if err != nil {
return resolvedShareTarget{}, err
}
if target.TelegramID == owner.TelegramID {
return resolvedShareTarget{}, domain.NewValidationError(
"You already own this list.",
)
}
if target.TelegramID > 0 {
allowed, err := s.botAccess.IsAllowed(ctx, target.TelegramID)
if err != nil {
return resolvedShareTarget{}, err
}
if !allowed {
return resolvedShareTarget{}, noBotAccessError()
}
user, found, err := s.users.GetByTelegramID(ctx, target.TelegramID)
if err != nil {
return resolvedShareTarget{}, err
}
if found {
if user.ID == access.List.OwnerID {
return resolvedShareTarget{}, domain.NewValidationError(
"You already own this list.",
)
}
return resolvedShareTarget{
userID: user.ID,
telegramID: user.TelegramID,
}, nil
}
return resolvedShareTarget{telegramID: target.TelegramID}, nil
}
telegramID, found, err := s.botAccess.TelegramIDForUsername(ctx, target.Username)
if err != nil {
return resolvedShareTarget{}, err
}
if !found {
return resolvedShareTarget{}, noBotAccessError()
}
if telegramID == owner.TelegramID {
return resolvedShareTarget{}, domain.NewValidationError(
"You already own this list.",
)
}
if telegramID > 0 {
user, found, err := s.users.GetByTelegramID(ctx, telegramID)
if err != nil {
return resolvedShareTarget{}, err
}
if found {
if user.ID == access.List.OwnerID {
return resolvedShareTarget{}, domain.NewValidationError(
"You already own this list.",
)
}
return resolvedShareTarget{
userID: user.ID,
telegramID: user.TelegramID,
username: target.Username,
}, nil
}
return resolvedShareTarget{
telegramID: telegramID,
username: target.Username,
}, nil
}
return resolvedShareTarget{username: target.Username}, nil
}
func noBotAccessError() error {
return domain.NewValidationError(
"That user does not have bot access. Use /allow first.",
)
}
func validShareRole(role domain.Role) bool {
return role == domain.RoleReader || role == domain.RoleEditor
}