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