834 lines
28 KiB
Go
834 lines
28 KiB
Go
package chatlists
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"unicode/utf8"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/links"
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
type Service struct {
|
|
chatlists store.ChatlistStore
|
|
dialogs store.DialogStore
|
|
channels ChannelService
|
|
premium PremiumChecker
|
|
newSlug func() (string, error)
|
|
}
|
|
|
|
type Option func(*Service)
|
|
|
|
// ChannelService is the domain-only channel dependency used for shared-folder
|
|
// peer membership side effects.
|
|
type ChannelService interface {
|
|
GetChannel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error)
|
|
InviteToChannel(ctx context.Context, userID, channelID int64, userIDs []int64, date int) (domain.CreateChannelResult, error)
|
|
JoinChannel(ctx context.Context, userID, channelID int64, date int) (domain.CreateChannelResult, error)
|
|
LeaveChannel(ctx context.Context, userID, channelID int64, date int) (domain.CreateChannelResult, error)
|
|
}
|
|
|
|
type PremiumChecker func(ctx context.Context, userID int64) bool
|
|
|
|
func WithChannels(channels ChannelService) Option {
|
|
return func(s *Service) {
|
|
s.channels = channels
|
|
}
|
|
}
|
|
|
|
func WithPremiumChecker(fn PremiumChecker) Option {
|
|
return func(s *Service) {
|
|
s.premium = fn
|
|
}
|
|
}
|
|
|
|
func WithSlugGenerator(fn func() (string, error)) Option {
|
|
return func(s *Service) {
|
|
if fn != nil {
|
|
s.newSlug = fn
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewService(chatlists store.ChatlistStore, dialogs store.DialogStore, opts ...Option) *Service {
|
|
s := &Service{
|
|
chatlists: chatlists,
|
|
dialogs: dialogs,
|
|
newSlug: randomChatlistSlug,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *Service) ExportInvite(ctx context.Context, userID int64, filterID int, title string, peers []domain.DialogFolderPeer, date int) (domain.DialogFolder, domain.ChatlistInvite, error) {
|
|
if err := validateChatlistUserFilter(userID, filterID); err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if utf8.RuneCountInString(title) > domain.MaxDialogFolderTitleRunes {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, domain.ErrChatlistInvalid
|
|
}
|
|
folder, err := s.ownerFolder(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if !folderShareable(folder) {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, domain.ErrChatlistNotShareable
|
|
}
|
|
selected, err := selectChatlistPeers(folder, peers, true)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if err := s.validateShareableChannelPeers(ctx, userID, selected); err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
count, err := s.chatlists.CountActiveInvites(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if count >= s.invitesLimit(ctx, userID) {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, domain.ErrChatlistInvitesTooMuch
|
|
}
|
|
folder = exportedChatlistFolder(folder, true)
|
|
var lastErr error
|
|
for i := 0; i < 8; i++ {
|
|
slug, err := s.newSlug()
|
|
if err != nil {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
invite := domain.ChatlistInvite{
|
|
OwnerUserID: userID,
|
|
FilterID: filterID,
|
|
Slug: slug,
|
|
Title: title,
|
|
Peers: selected,
|
|
Date: date,
|
|
}
|
|
saved, err := s.chatlists.SaveInvite(ctx, invite)
|
|
if err == nil {
|
|
if err := s.dialogs.UpsertFolder(ctx, userID, folder); err != nil {
|
|
rollbackErr := s.deleteInviteIfSaved(ctx, userID, filterID, saved.Slug)
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, errors.Join(fmt.Errorf("save exported chatlist folder: %w", err), rollbackErr)
|
|
}
|
|
return folder, saved, nil
|
|
}
|
|
if !errors.Is(err, domain.ErrChatlistSlugOccupied) {
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
lastErr = err
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = domain.ErrChatlistSlugOccupied
|
|
}
|
|
return domain.DialogFolder{}, domain.ChatlistInvite{}, lastErr
|
|
}
|
|
|
|
func (s *Service) ListInvites(ctx context.Context, userID int64, filterID int) ([]domain.ChatlistInvite, error) {
|
|
if err := validateChatlistUserFilter(userID, filterID); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := s.ownerFolder(ctx, userID, filterID); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.chatlists.ListInvites(ctx, userID, filterID)
|
|
}
|
|
|
|
func (s *Service) EditInvite(ctx context.Context, userID int64, filterID int, slug string, title *string, peers *[]domain.DialogFolderPeer, revoke bool) (domain.ChatlistInvite, error) {
|
|
if err := validateChatlistUserFilter(userID, filterID); err != nil {
|
|
return domain.ChatlistInvite{}, err
|
|
}
|
|
slug = CleanSlug(slug)
|
|
if !links.ValidChatlistSlug(slug) {
|
|
return domain.ChatlistInvite{}, domain.ErrChatlistInviteInvalid
|
|
}
|
|
folder, err := s.ownerFolder(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.ChatlistInvite{}, err
|
|
}
|
|
existing, found, err := s.chatlists.GetInvite(ctx, userID, filterID, slug)
|
|
if err != nil {
|
|
return domain.ChatlistInvite{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistInvite{}, domain.ErrChatlistInviteExpired
|
|
}
|
|
if title != nil {
|
|
if utf8.RuneCountInString(*title) > domain.MaxDialogFolderTitleRunes {
|
|
return domain.ChatlistInvite{}, domain.ErrChatlistInvalid
|
|
}
|
|
existing.Title = *title
|
|
}
|
|
if peers != nil {
|
|
selected, err := selectChatlistPeers(folder, *peers, true)
|
|
if err != nil {
|
|
return domain.ChatlistInvite{}, err
|
|
}
|
|
if err := s.validateShareableChannelPeers(ctx, userID, selected); err != nil {
|
|
return domain.ChatlistInvite{}, err
|
|
}
|
|
existing.Peers = selected
|
|
}
|
|
if revoke {
|
|
existing.Revoked = true
|
|
}
|
|
return s.chatlists.SaveInvite(ctx, existing)
|
|
}
|
|
|
|
func (s *Service) DeleteInvite(ctx context.Context, userID int64, filterID int, slug string) (domain.DialogFolder, bool, error) {
|
|
if err := validateChatlistUserFilter(userID, filterID); err != nil {
|
|
return domain.DialogFolder{}, false, err
|
|
}
|
|
slug = CleanSlug(slug)
|
|
if !links.ValidChatlistSlug(slug) {
|
|
return domain.DialogFolder{}, false, domain.ErrChatlistInviteInvalid
|
|
}
|
|
folder, err := s.ownerFolder(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, false, err
|
|
}
|
|
deleted, err := s.chatlists.DeleteInvite(ctx, userID, filterID, slug)
|
|
if err != nil || !deleted {
|
|
return domain.DialogFolder{}, false, err
|
|
}
|
|
count, err := s.chatlists.CountInvites(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, false, err
|
|
}
|
|
if count > 0 || !folder.HasMyInvites {
|
|
return domain.DialogFolder{}, false, nil
|
|
}
|
|
folder = exportedChatlistFolder(folder, false)
|
|
if err := s.dialogs.UpsertFolder(ctx, userID, folder); err != nil {
|
|
return domain.DialogFolder{}, false, fmt.Errorf("clear exported chatlist folder flag: %w", err)
|
|
}
|
|
return folder, true, nil
|
|
}
|
|
|
|
func (s *Service) CheckInvite(ctx context.Context, userID int64, slug string) (domain.ChatlistInvitePreview, error) {
|
|
if userID == 0 {
|
|
return domain.ChatlistInvitePreview{}, domain.ErrChatlistInvalid
|
|
}
|
|
slug = CleanSlug(slug)
|
|
if !links.ValidChatlistSlug(slug) {
|
|
return domain.ChatlistInvitePreview{}, domain.ErrChatlistInviteInvalid
|
|
}
|
|
invite, folder, err := s.inviteWithFolder(ctx, slug)
|
|
if err != nil {
|
|
return domain.ChatlistInvitePreview{}, err
|
|
}
|
|
out := domain.ChatlistInvitePreview{Invite: invite, OwnerFolder: folder}
|
|
if userID == invite.OwnerUserID {
|
|
ownerFolder := exportedChatlistFolder(folder, true)
|
|
out.LocalFolder = &ownerFolder
|
|
out.Already = peersIntersection(invite.Peers, folderPeers(ownerFolder))
|
|
return out, nil
|
|
}
|
|
membership, found, err := s.chatlists.GetMembershipBySlug(ctx, userID, slug)
|
|
if err != nil {
|
|
return domain.ChatlistInvitePreview{}, err
|
|
}
|
|
if !found {
|
|
out.Missing = cloneFolderPeers(invite.Peers)
|
|
return out, nil
|
|
}
|
|
local, found, err := s.dialogs.GetFolder(ctx, userID, membership.LocalFilterID)
|
|
if err != nil {
|
|
return domain.ChatlistInvitePreview{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistInvitePreview{}, domain.ErrChatlistInvalid
|
|
}
|
|
out.Membership = &membership
|
|
out.LocalFolder = &local
|
|
out.Already = peersIntersection(invite.Peers, folderPeers(local))
|
|
out.Missing = peersDifference(invite.Peers, folderPeers(local))
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) JoinInvite(ctx context.Context, userID int64, slug string, peers []domain.DialogFolderPeer, date int) (domain.ChatlistJoinResult, error) {
|
|
if userID == 0 {
|
|
return domain.ChatlistJoinResult{}, domain.ErrChatlistInvalid
|
|
}
|
|
slug = CleanSlug(slug)
|
|
if !links.ValidChatlistSlug(slug) {
|
|
return domain.ChatlistJoinResult{}, domain.ErrChatlistInviteInvalid
|
|
}
|
|
invite, ownerFolder, err := s.inviteWithFolder(ctx, slug)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
if userID == invite.OwnerUserID {
|
|
return domain.ChatlistJoinResult{Folder: exportedChatlistFolder(ownerFolder, true), Date: date}, nil
|
|
}
|
|
if existing, found, err := s.chatlists.GetMembershipBySlug(ctx, userID, slug); err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
} else if found {
|
|
folder, found, err := s.dialogs.GetFolder(ctx, userID, existing.LocalFilterID)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistJoinResult{}, domain.ErrChatlistInvalid
|
|
}
|
|
return domain.ChatlistJoinResult{Folder: folder, Membership: existing, Date: date}, nil
|
|
}
|
|
selected, err := selectInvitePeers(invite, peers, true)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
count, err := s.chatlists.CountMemberships(ctx, userID)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
if count >= s.joinedLimit(ctx, userID) {
|
|
return domain.ChatlistJoinResult{}, domain.ErrChatlistsTooMuch
|
|
}
|
|
channelResults, err := s.joinChannelPeers(ctx, invite.OwnerUserID, userID, selected, date)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
filterID, err := s.nextLocalFilterID(ctx, userID)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
folder := importedChatlistFolder(ownerFolder, filterID, selected)
|
|
membership := domain.ChatlistMembership{
|
|
UserID: userID,
|
|
LocalFilterID: filterID,
|
|
OwnerUserID: invite.OwnerUserID,
|
|
OwnerFilterID: invite.FilterID,
|
|
Slug: invite.Slug,
|
|
Date: date,
|
|
}
|
|
if err := s.dialogs.UpsertFolder(ctx, userID, folder); err != nil {
|
|
if rollbackErr := s.leaveJoinedChannelResults(ctx, userID, channelResults, date); rollbackErr != nil {
|
|
return domain.ChatlistJoinResult{}, errors.Join(fmt.Errorf("save joined chatlist folder: %w", err), rollbackErr)
|
|
}
|
|
return domain.ChatlistJoinResult{}, fmt.Errorf("save joined chatlist folder: %w", err)
|
|
}
|
|
if err := s.chatlists.SaveMembership(ctx, membership); err != nil {
|
|
rollbackErr := errors.Join(
|
|
s.dialogs.DeleteFolder(ctx, userID, filterID),
|
|
s.leaveJoinedChannelResults(ctx, userID, channelResults, date),
|
|
)
|
|
return domain.ChatlistJoinResult{}, errors.Join(err, rollbackErr)
|
|
}
|
|
return domain.ChatlistJoinResult{Folder: folder, Membership: membership, Date: date, ChannelResults: channelResults}, nil
|
|
}
|
|
|
|
func (s *Service) GetUpdates(ctx context.Context, userID int64, localFilterID int) (domain.ChatlistUpdates, error) {
|
|
membership, folder, invite, err := s.memberFolderInvite(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrChatlistInviteExpired) {
|
|
return domain.ChatlistUpdates{Membership: membership}, nil
|
|
}
|
|
if errors.Is(err, domain.ErrChatlistInvalid) {
|
|
if owner, ownerErr := s.ownerFolder(ctx, userID, localFilterID); ownerErr == nil && owner.IsChatlist && owner.HasMyInvites {
|
|
return domain.ChatlistUpdates{}, nil
|
|
}
|
|
}
|
|
return domain.ChatlistUpdates{}, err
|
|
}
|
|
if membership.HiddenUpdates {
|
|
return domain.ChatlistUpdates{Membership: membership}, nil
|
|
}
|
|
return domain.ChatlistUpdates{
|
|
Membership: membership,
|
|
Missing: peersDifference(invite.Peers, folderPeers(folder)),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) JoinUpdates(ctx context.Context, userID int64, localFilterID int, peers []domain.DialogFolderPeer, date int) (domain.ChatlistJoinResult, error) {
|
|
membership, folder, invite, err := s.memberFolderInvite(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
selected, err := selectInvitePeers(invite, peers, true)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
if _, err := s.chatlists.SetMembershipHidden(ctx, userID, membership.LocalFilterID, false); err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
channelResults, err := s.joinChannelPeers(ctx, membership.OwnerUserID, userID, selected, date)
|
|
if err != nil {
|
|
return domain.ChatlistJoinResult{}, err
|
|
}
|
|
folder.IncludePeers = mergeFolderPeers(folder.IncludePeers, selected)
|
|
folder.IsChatlist = true
|
|
if err := s.dialogs.UpsertFolder(ctx, userID, folder); err != nil {
|
|
if rollbackErr := s.leaveJoinedChannelResults(ctx, userID, channelResults, date); rollbackErr != nil {
|
|
return domain.ChatlistJoinResult{}, errors.Join(fmt.Errorf("save chatlist updates: %w", err), rollbackErr)
|
|
}
|
|
return domain.ChatlistJoinResult{}, fmt.Errorf("save chatlist updates: %w", err)
|
|
}
|
|
return domain.ChatlistJoinResult{Folder: folder, Membership: membership, ChannelResults: channelResults}, nil
|
|
}
|
|
|
|
func (s *Service) HideUpdates(ctx context.Context, userID int64, localFilterID int) error {
|
|
if err := validateChatlistUserFilter(userID, localFilterID); err != nil {
|
|
return err
|
|
}
|
|
if _, found, err := s.chatlists.GetMembershipByLocalFilter(ctx, userID, localFilterID); err != nil {
|
|
return err
|
|
} else if !found {
|
|
return domain.ErrChatlistInvalid
|
|
}
|
|
_, err := s.chatlists.SetMembershipHidden(ctx, userID, localFilterID, true)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) Leave(ctx context.Context, userID int64, localFilterID int, peers []domain.DialogFolderPeer, date int) (domain.ChatlistLeaveResult, error) {
|
|
if err := validateChatlistUserFilter(userID, localFilterID); err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
}
|
|
if _, found, err := s.chatlists.GetMembershipByLocalFilter(ctx, userID, localFilterID); err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
} else if !found {
|
|
return domain.ChatlistLeaveResult{}, domain.ErrChatlistInvalid
|
|
}
|
|
folder, found, err := s.dialogs.GetFolder(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistLeaveResult{}, domain.ErrChatlistInvalid
|
|
}
|
|
selected, err := selectPeersFromAllowed(folderPeerMap(channelFolderPeers(folderPeers(folder))), peers, false)
|
|
if err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
}
|
|
channelResults, err := s.leaveChannelPeers(ctx, userID, selected, date)
|
|
if err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
}
|
|
if err := s.dialogs.DeleteFolder(ctx, userID, localFilterID); err != nil {
|
|
return domain.ChatlistLeaveResult{}, err
|
|
}
|
|
if _, err := s.chatlists.DeleteMembershipByLocalFilter(ctx, userID, localFilterID); err != nil {
|
|
restoreErr := s.dialogs.UpsertFolder(ctx, userID, folder)
|
|
return domain.ChatlistLeaveResult{}, errors.Join(err, restoreErr)
|
|
}
|
|
return domain.ChatlistLeaveResult{FilterID: localFilterID, ChannelResults: channelResults, RequestedLeaves: selected}, nil
|
|
}
|
|
|
|
func (s *Service) LeaveSuggestions(ctx context.Context, userID int64, localFilterID int) ([]domain.DialogFolderPeer, error) {
|
|
if err := validateChatlistUserFilter(userID, localFilterID); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, found, err := s.chatlists.GetMembershipByLocalFilter(ctx, userID, localFilterID); err != nil {
|
|
return nil, err
|
|
} else if !found {
|
|
return nil, domain.ErrChatlistInvalid
|
|
}
|
|
folder, found, err := s.dialogs.GetFolder(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, domain.ErrChatlistInvalid
|
|
}
|
|
return channelFolderPeers(folderPeers(folder)), nil
|
|
}
|
|
|
|
func (s *Service) ownerFolder(ctx context.Context, userID int64, filterID int) (domain.DialogFolder, error) {
|
|
if s == nil || s.chatlists == nil || s.dialogs == nil {
|
|
return domain.DialogFolder{}, domain.ErrChatlistInvalid
|
|
}
|
|
folder, found, err := s.dialogs.GetFolder(ctx, userID, filterID)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, err
|
|
}
|
|
if !found {
|
|
return domain.DialogFolder{}, domain.ErrChatlistInvalid
|
|
}
|
|
folder.ID = filterID
|
|
return folder, nil
|
|
}
|
|
|
|
func (s *Service) inviteWithFolder(ctx context.Context, slug string) (domain.ChatlistInvite, domain.DialogFolder, error) {
|
|
invite, found, err := s.chatlists.GetInviteBySlug(ctx, slug)
|
|
if err != nil {
|
|
return domain.ChatlistInvite{}, domain.DialogFolder{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistInvite{}, domain.DialogFolder{}, domain.ErrChatlistInviteExpired
|
|
}
|
|
folder, found, err := s.dialogs.GetFolder(ctx, invite.OwnerUserID, invite.FilterID)
|
|
if err != nil {
|
|
return domain.ChatlistInvite{}, domain.DialogFolder{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistInvite{}, domain.DialogFolder{}, domain.ErrChatlistInviteExpired
|
|
}
|
|
return invite, exportedChatlistFolder(folder, true), nil
|
|
}
|
|
|
|
func (s *Service) memberFolderInvite(ctx context.Context, userID int64, localFilterID int) (domain.ChatlistMembership, domain.DialogFolder, domain.ChatlistInvite, error) {
|
|
if err := validateChatlistUserFilter(userID, localFilterID); err != nil {
|
|
return domain.ChatlistMembership{}, domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
membership, found, err := s.chatlists.GetMembershipByLocalFilter(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
return domain.ChatlistMembership{}, domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if !found {
|
|
return domain.ChatlistMembership{}, domain.DialogFolder{}, domain.ChatlistInvite{}, domain.ErrChatlistInvalid
|
|
}
|
|
folder, found, err := s.dialogs.GetFolder(ctx, userID, localFilterID)
|
|
if err != nil {
|
|
return membership, domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if !found {
|
|
return membership, domain.DialogFolder{}, domain.ChatlistInvite{}, domain.ErrChatlistInvalid
|
|
}
|
|
invite, found, err := s.chatlists.GetInviteBySlug(ctx, membership.Slug)
|
|
if err != nil {
|
|
return membership, domain.DialogFolder{}, domain.ChatlistInvite{}, err
|
|
}
|
|
if !found {
|
|
return membership, folder, domain.ChatlistInvite{}, domain.ErrChatlistInviteExpired
|
|
}
|
|
return membership, folder, invite, nil
|
|
}
|
|
|
|
func (s *Service) nextLocalFilterID(ctx context.Context, userID int64) (int, error) {
|
|
list, err := s.dialogs.ListFolders(ctx, userID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
used := make(map[int]struct{}, len(list.Folders))
|
|
for _, folder := range list.Folders {
|
|
used[folder.ID] = struct{}{}
|
|
}
|
|
for id := domain.DialogCustomFolderMinID; id < domain.DialogCustomFolderMinID+domain.MaxDialogFolders; id++ {
|
|
if _, ok := used[id]; !ok {
|
|
return id, nil
|
|
}
|
|
}
|
|
return 0, domain.ErrChatlistsTooMuch
|
|
}
|
|
|
|
func validateChatlistUserFilter(userID int64, filterID int) error {
|
|
if userID == 0 || filterID < domain.DialogCustomFolderMinID {
|
|
return domain.ErrChatlistInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func folderShareable(folder domain.DialogFolder) bool {
|
|
if len(folder.ExcludePeers) > 0 || folder.Contacts || folder.NonContacts || folder.Groups ||
|
|
folder.Broadcasts || folder.Bots || folder.ExcludeMuted || folder.ExcludeRead || folder.ExcludeArchived {
|
|
return false
|
|
}
|
|
return len(folder.IncludePeers)+len(folder.PinnedPeers) > 0
|
|
}
|
|
|
|
func (s *Service) validateShareableChannelPeers(ctx context.Context, userID int64, peers []domain.DialogFolderPeer) error {
|
|
if len(peers) == 0 {
|
|
return domain.ErrChatlistPeersEmpty
|
|
}
|
|
for _, item := range peers {
|
|
if item.Peer.Type != domain.PeerTypeChannel || item.Peer.ID == 0 {
|
|
return domain.ErrChatlistNotShareable
|
|
}
|
|
if s.channels == nil {
|
|
continue
|
|
}
|
|
view, err := s.channels.GetChannel(ctx, userID, item.Peer.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !channelShareableForChatlist(view) {
|
|
return domain.ErrChatlistNotShareable
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func channelShareableForChatlist(view domain.ChannelView) bool {
|
|
if view.Channel.Deleted || view.Forbidden {
|
|
return false
|
|
}
|
|
return channelCanInviteForChatlist(view) || channelPublicJoinableForChatlist(view)
|
|
}
|
|
|
|
func channelCanInviteForChatlist(view domain.ChannelView) bool {
|
|
switch view.Self.Role {
|
|
case domain.ChannelRoleCreator:
|
|
return true
|
|
case domain.ChannelRoleAdmin:
|
|
if view.Self.AdminRights.InviteUsers {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func channelPublicJoinableForChatlist(view domain.ChannelView) bool {
|
|
if view.Channel.Deleted || view.Forbidden {
|
|
return false
|
|
}
|
|
return view.Channel.Username != "" && !view.Channel.JoinRequest
|
|
}
|
|
|
|
func (s *Service) joinChannelPeers(ctx context.Context, ownerUserID, userID int64, peers []domain.DialogFolderPeer, date int) ([]domain.CreateChannelResult, error) {
|
|
if s.channels == nil {
|
|
return nil, nil
|
|
}
|
|
type joinPlan struct {
|
|
peer domain.DialogFolderPeer
|
|
useInvite bool
|
|
}
|
|
channelPeers := channelFolderPeers(peers)
|
|
plans := make([]joinPlan, 0, len(channelPeers))
|
|
for _, item := range channelPeers {
|
|
view, err := s.channels.GetChannel(ctx, ownerUserID, item.Peer.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !channelShareableForChatlist(view) {
|
|
return nil, domain.ErrChatlistNotShareable
|
|
}
|
|
plans = append(plans, joinPlan{peer: item, useInvite: channelCanInviteForChatlist(view)})
|
|
}
|
|
results := make([]domain.CreateChannelResult, 0, len(plans))
|
|
for _, plan := range plans {
|
|
var res domain.CreateChannelResult
|
|
var err error
|
|
if plan.useInvite {
|
|
res, err = s.channels.InviteToChannel(ctx, ownerUserID, plan.peer.Peer.ID, []int64{userID}, date)
|
|
} else {
|
|
res, err = s.channels.JoinChannel(ctx, userID, plan.peer.Peer.ID, date)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrUserAlreadyParticipant) {
|
|
continue
|
|
}
|
|
return results, err
|
|
}
|
|
results = append(results, res)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (s *Service) leaveChannelPeers(ctx context.Context, userID int64, peers []domain.DialogFolderPeer, date int) ([]domain.CreateChannelResult, error) {
|
|
if s.channels == nil {
|
|
return nil, nil
|
|
}
|
|
results := make([]domain.CreateChannelResult, 0)
|
|
for _, item := range channelFolderPeers(peers) {
|
|
res, err := s.channels.LeaveChannel(ctx, userID, item.Peer.ID, date)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrUserNotParticipant) || errors.Is(err, domain.ErrChannelPrivate) {
|
|
continue
|
|
}
|
|
return results, err
|
|
}
|
|
results = append(results, res)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (s *Service) leaveJoinedChannelResults(ctx context.Context, userID int64, results []domain.CreateChannelResult, date int) error {
|
|
if s.channels == nil || len(results) == 0 {
|
|
return nil
|
|
}
|
|
var joinedErr error
|
|
for _, res := range results {
|
|
if res.Channel.ID == 0 {
|
|
continue
|
|
}
|
|
if _, err := s.channels.LeaveChannel(ctx, userID, res.Channel.ID, date); err != nil &&
|
|
!errors.Is(err, domain.ErrUserNotParticipant) &&
|
|
!errors.Is(err, domain.ErrChannelPrivate) {
|
|
joinedErr = errors.Join(joinedErr, err)
|
|
}
|
|
}
|
|
return joinedErr
|
|
}
|
|
|
|
func (s *Service) deleteInviteIfSaved(ctx context.Context, ownerUserID int64, filterID int, slug string) error {
|
|
if slug == "" {
|
|
return nil
|
|
}
|
|
if _, err := s.chatlists.DeleteInvite(ctx, ownerUserID, filterID, slug); err != nil {
|
|
return fmt.Errorf("rollback chatlist invite: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) invitesLimit(ctx context.Context, userID int64) int {
|
|
if s != nil && s.premium != nil && s.premium(ctx, userID) {
|
|
return domain.MaxChatlistInvitesPremium
|
|
}
|
|
return domain.MaxChatlistInvitesDefault
|
|
}
|
|
|
|
func (s *Service) joinedLimit(ctx context.Context, userID int64) int {
|
|
if s != nil && s.premium != nil && s.premium(ctx, userID) {
|
|
return domain.MaxChatlistsJoinedPremium
|
|
}
|
|
return domain.MaxChatlistsJoinedDefault
|
|
}
|
|
|
|
func exportedChatlistFolder(folder domain.DialogFolder, hasMyInvites bool) domain.DialogFolder {
|
|
folder.Contacts = false
|
|
folder.NonContacts = false
|
|
folder.Groups = false
|
|
folder.Broadcasts = false
|
|
folder.Bots = false
|
|
folder.ExcludeMuted = false
|
|
folder.ExcludeRead = false
|
|
folder.ExcludeArchived = false
|
|
folder.ExcludePeers = nil
|
|
folder.IsChatlist = true
|
|
folder.HasMyInvites = hasMyInvites
|
|
return cloneDialogFolder(folder)
|
|
}
|
|
|
|
func importedChatlistFolder(source domain.DialogFolder, filterID int, peers []domain.DialogFolderPeer) domain.DialogFolder {
|
|
source = exportedChatlistFolder(source, false)
|
|
source.ID = filterID
|
|
source.PinnedPeers = nil
|
|
source.IncludePeers = cloneFolderPeers(peers)
|
|
source.HasMyInvites = false
|
|
return source
|
|
}
|
|
|
|
func selectChatlistPeers(folder domain.DialogFolder, requested []domain.DialogFolderPeer, requireNonEmpty bool) ([]domain.DialogFolderPeer, error) {
|
|
allowed := folderPeerMap(folderPeers(folder))
|
|
return selectPeersFromAllowed(allowed, requested, requireNonEmpty)
|
|
}
|
|
|
|
func selectInvitePeers(invite domain.ChatlistInvite, requested []domain.DialogFolderPeer, requireNonEmpty bool) ([]domain.DialogFolderPeer, error) {
|
|
if len(requested) == 0 {
|
|
requested = invite.Peers
|
|
}
|
|
return selectPeersFromAllowed(folderPeerMap(invite.Peers), requested, requireNonEmpty)
|
|
}
|
|
|
|
func selectPeersFromAllowed(allowed map[domain.Peer]domain.DialogFolderPeer, requested []domain.DialogFolderPeer, requireNonEmpty bool) ([]domain.DialogFolderPeer, error) {
|
|
if len(requested) > domain.MaxChatlistInvitePeers {
|
|
return nil, domain.ErrChatlistPeersTooMuch
|
|
}
|
|
out := make([]domain.DialogFolderPeer, 0, len(requested))
|
|
seen := make(map[domain.Peer]struct{}, len(requested))
|
|
for _, item := range requested {
|
|
if item.Peer.Type == "" || item.Peer.ID == 0 {
|
|
return nil, domain.ErrChatlistInvalid
|
|
}
|
|
allowedPeer, ok := allowed[item.Peer]
|
|
if !ok {
|
|
return nil, domain.ErrChatlistInvalid
|
|
}
|
|
if _, ok := seen[item.Peer]; ok {
|
|
continue
|
|
}
|
|
seen[item.Peer] = struct{}{}
|
|
if item.AccessHash == 0 {
|
|
item.AccessHash = allowedPeer.AccessHash
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if requireNonEmpty && len(out) == 0 {
|
|
return nil, domain.ErrChatlistPeersEmpty
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func channelFolderPeers(peers []domain.DialogFolderPeer) []domain.DialogFolderPeer {
|
|
out := make([]domain.DialogFolderPeer, 0, len(peers))
|
|
for _, item := range peers {
|
|
if item.Peer.Type == domain.PeerTypeChannel && item.Peer.ID != 0 {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func folderPeers(folder domain.DialogFolder) []domain.DialogFolderPeer {
|
|
return mergeFolderPeers(folder.PinnedPeers, folder.IncludePeers)
|
|
}
|
|
|
|
func folderPeerMap(peers []domain.DialogFolderPeer) map[domain.Peer]domain.DialogFolderPeer {
|
|
out := make(map[domain.Peer]domain.DialogFolderPeer, len(peers))
|
|
for _, item := range peers {
|
|
if item.Peer.Type == "" || item.Peer.ID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := out[item.Peer]; !ok {
|
|
out[item.Peer] = item
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mergeFolderPeers(a, b []domain.DialogFolderPeer) []domain.DialogFolderPeer {
|
|
out := make([]domain.DialogFolderPeer, 0, len(a)+len(b))
|
|
seen := make(map[domain.Peer]struct{}, len(a)+len(b))
|
|
for _, list := range [][]domain.DialogFolderPeer{a, b} {
|
|
for _, item := range list {
|
|
if item.Peer.Type == "" || item.Peer.ID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[item.Peer]; ok {
|
|
continue
|
|
}
|
|
seen[item.Peer] = struct{}{}
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func peersDifference(all, existing []domain.DialogFolderPeer) []domain.DialogFolderPeer {
|
|
existingMap := folderPeerMap(existing)
|
|
out := make([]domain.DialogFolderPeer, 0)
|
|
for _, item := range all {
|
|
if _, ok := existingMap[item.Peer]; !ok {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func peersIntersection(all, existing []domain.DialogFolderPeer) []domain.DialogFolderPeer {
|
|
existingMap := folderPeerMap(existing)
|
|
out := make([]domain.DialogFolderPeer, 0)
|
|
for _, item := range all {
|
|
if _, ok := existingMap[item.Peer]; ok {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneDialogFolder(folder domain.DialogFolder) domain.DialogFolder {
|
|
folder.TitleEntities = append([]domain.MessageEntity(nil), folder.TitleEntities...)
|
|
folder.PinnedPeers = cloneFolderPeers(folder.PinnedPeers)
|
|
folder.IncludePeers = cloneFolderPeers(folder.IncludePeers)
|
|
folder.ExcludePeers = cloneFolderPeers(folder.ExcludePeers)
|
|
return folder
|
|
}
|
|
|
|
func cloneFolderPeers(peers []domain.DialogFolderPeer) []domain.DialogFolderPeer {
|
|
return append([]domain.DialogFolderPeer(nil), peers...)
|
|
}
|
|
|
|
func CleanSlug(raw string) string {
|
|
return links.CleanChatlistSlug(raw)
|
|
}
|
|
|
|
func randomChatlistSlug() (string, error) {
|
|
var b [12]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return "", fmt.Errorf("chatlist slug rand: %w", err)
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
|
}
|