chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
560
internal/app/stories/service.go
Normal file
560
internal/app/stories/service.go
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
package stories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// Service owns Telegram story read/watch state.
|
||||
type Service struct {
|
||||
stories store.StoryStore
|
||||
channelAccess ChannelStoryAccess
|
||||
}
|
||||
|
||||
// ChannelStoryAccess validates channel story mutation rights without leaking TL
|
||||
// types into the app layer.
|
||||
type ChannelStoryAccess interface {
|
||||
CanPostStory(ctx context.Context, userID, channelID int64) error
|
||||
CanEditStory(ctx context.Context, userID, channelID int64) error
|
||||
CanDeleteStory(ctx context.Context, userID, channelID int64) error
|
||||
CanPinStory(ctx context.Context, userID, channelID int64) error
|
||||
}
|
||||
|
||||
// Option adjusts optional story service dependencies.
|
||||
type Option func(*Service)
|
||||
|
||||
// WithChannelStoryAccess enables channel story publishing/admin checks.
|
||||
func WithChannelStoryAccess(access ChannelStoryAccess) Option {
|
||||
return func(s *Service) { s.channelAccess = access }
|
||||
}
|
||||
|
||||
// NewService creates a story service.
|
||||
func NewService(stories store.StoryStore, opts ...Option) *Service {
|
||||
s := &Service{stories: stories}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) CreateStory(ctx context.Context, userID int64, req domain.StoryCreateRequest) (domain.StoryCreateResult, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, userID, req.Owner, storyOwnerPost); err != nil {
|
||||
return domain.StoryCreateResult{}, err
|
||||
}
|
||||
if req.RandomID == 0 {
|
||||
return domain.StoryCreateResult{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if req.Media == nil {
|
||||
return domain.StoryCreateResult{}, domain.ErrStoryNotFound
|
||||
}
|
||||
req.Period = normalizeStoryPeriod(req.Period)
|
||||
if req.Period == 0 {
|
||||
return domain.StoryCreateResult{}, domain.ErrStoryPeriodInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryCreateResult{}, domain.ErrStoryNotFound
|
||||
}
|
||||
return s.stories.CreateStory(ctx, req)
|
||||
}
|
||||
|
||||
// UpsertStory stores a story snapshot. It is used by tests now and by the
|
||||
// publishing flow once sendStory/editStory are promoted from stubs.
|
||||
func (s *Service) UpsertStory(ctx context.Context, req domain.UpsertStoryRequest) (domain.Story, error) {
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.Story{}, domain.ErrStoryNotFound
|
||||
}
|
||||
return s.stories.UpsertStory(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) GetAllStories(ctx context.Context, viewerUserID int64, hidden bool, now, limit int) (domain.StoryList, error) {
|
||||
return s.GetAllStoriesPage(ctx, viewerUserID, hidden, now, domain.StoryListCursor{}, limit)
|
||||
}
|
||||
|
||||
func (s *Service) GetAllStoriesPage(ctx context.Context, viewerUserID int64, hidden bool, now int, cursor domain.StoryListCursor, limit int) (domain.StoryList, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryList{}, nil
|
||||
}
|
||||
if cursor.Set {
|
||||
if err := validateStoryListCursor(cursor); err != nil {
|
||||
return domain.StoryList{}, domain.ErrStoryOffsetInvalid
|
||||
}
|
||||
}
|
||||
return s.stories.ListActiveStoriesPage(ctx, viewerUserID, hidden, now, cursor, clampStoryLimit(limit))
|
||||
}
|
||||
|
||||
func (s *Service) GetAllStoriesDigest(ctx context.Context, viewerUserID int64, hidden bool, now int) (domain.StoryListDigest, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryListDigest{}, nil
|
||||
}
|
||||
return s.stories.ActiveStoriesDigest(ctx, viewerUserID, hidden, now)
|
||||
}
|
||||
|
||||
func (s *Service) ListOwnerActiveStories(ctx context.Context, userID int64, owner domain.Peer, now, limit int) (domain.StoryList, error) {
|
||||
if err := validateStoryOwner(userID, owner); err != nil {
|
||||
return domain.StoryList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryList{}, nil
|
||||
}
|
||||
return s.stories.ListOwnerActiveStories(ctx, owner, now, clampStoryLimit(limit))
|
||||
}
|
||||
|
||||
func (s *Service) GetPeerStories(ctx context.Context, viewerUserID int64, peer domain.Peer, now int) (domain.PeerStories, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.PeerStories{Peer: peer}, nil
|
||||
}
|
||||
return s.stories.GetPeerStories(ctx, viewerUserID, peer, now)
|
||||
}
|
||||
|
||||
func (s *Service) GetStoriesByID(ctx context.Context, viewerUserID int64, peer domain.Peer, ids []int, now int) (domain.StoryList, error) {
|
||||
ids, err := normalizeStoryIDsNonEmpty(ids)
|
||||
if err != nil {
|
||||
return domain.StoryList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryList{}, nil
|
||||
}
|
||||
return s.stories.GetStoriesByID(ctx, viewerUserID, peer, ids, now)
|
||||
}
|
||||
|
||||
func (s *Service) GetStoriesArchive(ctx context.Context, viewerUserID int64, peer domain.Peer, offsetID, limit, now int) (domain.StoryList, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, viewerUserID, peer, storyOwnerEdit); err != nil {
|
||||
return domain.StoryList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryList{}, nil
|
||||
}
|
||||
return s.stories.ListStoriesArchive(ctx, viewerUserID, peer, offsetID, clampStoryArchiveLimit(limit), now)
|
||||
}
|
||||
|
||||
func (s *Service) GetPinnedStories(ctx context.Context, viewerUserID int64, peer domain.Peer, offsetID, limit, now int) (domain.StoryList, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryList{}, nil
|
||||
}
|
||||
return s.stories.ListPinnedStories(ctx, viewerUserID, peer, offsetID, clampStoryLimit(limit), now)
|
||||
}
|
||||
|
||||
func (s *Service) HasPinnedStories(ctx context.Context, viewerUserID int64, peer domain.Peer, now int) (bool, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return false, nil
|
||||
}
|
||||
return s.stories.HasPinnedStories(ctx, viewerUserID, peer, now)
|
||||
}
|
||||
|
||||
func (s *Service) ListReadStates(ctx context.Context, viewerUserID int64) ([]domain.StoryReadState, error) {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.stories.ListReadStates(ctx, viewerUserID)
|
||||
}
|
||||
|
||||
func (s *Service) GetPeerMaxIDs(ctx context.Context, viewerUserID int64, peers []domain.Peer, now int) ([]domain.RecentStory, error) {
|
||||
if len(peers) > domain.MaxStoryIDs {
|
||||
return nil, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 || len(peers) == 0 {
|
||||
out := make([]domain.RecentStory, len(peers))
|
||||
for i, peer := range peers {
|
||||
out[i].Peer = peer
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return s.stories.GetPeerMaxIDs(ctx, viewerUserID, peers, now)
|
||||
}
|
||||
|
||||
func (s *Service) GetPeerHiddenStates(ctx context.Context, viewerUserID int64, peers []domain.Peer) (map[domain.Peer]bool, error) {
|
||||
if len(peers) > domain.MaxStoryIDs {
|
||||
return nil, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 || len(peers) == 0 {
|
||||
return map[domain.Peer]bool{}, nil
|
||||
}
|
||||
return s.stories.GetPeerHiddenStates(ctx, viewerUserID, peers)
|
||||
}
|
||||
|
||||
func (s *Service) GetPeerStoryProjections(ctx context.Context, viewerUserID int64, peers []domain.Peer, now int) ([]domain.PeerStoryProjection, error) {
|
||||
if len(peers) > domain.MaxStoryIDs {
|
||||
return nil, domain.ErrStoryIDInvalid
|
||||
}
|
||||
out := make([]domain.PeerStoryProjection, len(peers))
|
||||
for i, peer := range peers {
|
||||
out[i].Peer = peer
|
||||
out[i].Recent.Peer = peer
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 || len(peers) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
return s.stories.GetPeerStoryProjections(ctx, viewerUserID, peers, now)
|
||||
}
|
||||
|
||||
func (s *Service) ReadStories(ctx context.Context, viewerUserID int64, peer domain.Peer, maxID, date int) (domain.StoryReadResult, error) {
|
||||
if maxID <= 0 || maxID > domain.MaxStoryID {
|
||||
return domain.StoryReadResult{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryReadResult{ViewerID: viewerUserID, Peer: peer, Date: date}, nil
|
||||
}
|
||||
recent, err := s.stories.GetPeerMaxIDs(ctx, viewerUserID, []domain.Peer{peer}, date)
|
||||
if err != nil {
|
||||
return domain.StoryReadResult{}, err
|
||||
}
|
||||
if len(recent) == 0 || recent[0].MaxID <= 0 {
|
||||
return domain.StoryReadResult{ViewerID: viewerUserID, Peer: peer, Date: date}, nil
|
||||
}
|
||||
if maxID > recent[0].MaxID {
|
||||
maxID = recent[0].MaxID
|
||||
}
|
||||
return s.stories.MarkRead(ctx, viewerUserID, peer, maxID, date)
|
||||
}
|
||||
|
||||
func (s *Service) IncrementViews(ctx context.Context, viewerUserID int64, peer domain.Peer, ids []int, date int) (int, error) {
|
||||
ids, err := normalizeStoryIDsNonEmpty(ids)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return s.stories.IncrementViews(ctx, viewerUserID, peer, ids, date)
|
||||
}
|
||||
|
||||
func (s *Service) SendReaction(ctx context.Context, viewerUserID int64, peer domain.Peer, storyID int, reaction *domain.MessageReaction, date int) (domain.StoryReactionResult, error) {
|
||||
if storyID <= 0 || storyID > domain.MaxStoryID {
|
||||
return domain.StoryReactionResult{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return domain.StoryReactionResult{ViewerID: viewerUserID, Peer: peer, StoryID: storyID, Reaction: reaction, Date: date}, nil
|
||||
}
|
||||
return s.stories.SetReaction(ctx, viewerUserID, peer, storyID, reaction, date)
|
||||
}
|
||||
|
||||
func (s *Service) GetStoryViewsList(ctx context.Context, viewerUserID int64, req domain.StoryViewListRequest) (domain.StoryViewList, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, viewerUserID, req.Owner, storyOwnerEdit); err != nil {
|
||||
return domain.StoryViewList{}, err
|
||||
}
|
||||
if req.StoryID <= 0 || req.StoryID > domain.MaxStoryID {
|
||||
return domain.StoryViewList{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if err := domain.ValidateStoryInteractionOffset(req.Offset, false); err != nil {
|
||||
return domain.StoryViewList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryViewList{}, nil
|
||||
}
|
||||
req.ViewerUserID = viewerUserID
|
||||
req.Limit = clampStoryInteractionLimit(req.Limit)
|
||||
req.Query = normalizeStoryViewQuery(req.Query)
|
||||
return s.stories.ListStoryViews(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) GetStoryReactionsList(ctx context.Context, viewerUserID int64, req domain.StoryReactionListRequest) (domain.StoryReactionList, error) {
|
||||
if req.StoryID <= 0 || req.StoryID > domain.MaxStoryID {
|
||||
return domain.StoryReactionList{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if req.Owner.Type == domain.PeerTypeChannel {
|
||||
if viewerUserID == 0 || !req.CanViewOwnerInteractions {
|
||||
return domain.StoryReactionList{}, domain.ErrStoryPeerInvalid
|
||||
}
|
||||
} else {
|
||||
return domain.StoryReactionList{}, domain.ErrStoryPeerInvalid
|
||||
}
|
||||
if err := domain.ValidateStoryReactionInteractionOffset(req.Offset, req.ForwardsFirst); err != nil {
|
||||
return domain.StoryReactionList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryReactionList{}, nil
|
||||
}
|
||||
req.ViewerUserID = viewerUserID
|
||||
req.Limit = clampStoryInteractionLimit(req.Limit)
|
||||
return s.stories.ListStoryReactions(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) GetStoryPublicForwards(ctx context.Context, viewerUserID int64, req domain.StoryPublicForwardListRequest) (domain.StoryPublicForwardList, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, viewerUserID, req.Owner, storyOwnerEdit); err != nil {
|
||||
return domain.StoryPublicForwardList{}, err
|
||||
}
|
||||
if req.StoryID <= 0 || req.StoryID > domain.MaxStoryID {
|
||||
return domain.StoryPublicForwardList{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if err := domain.ValidateStoryInteractionOffset(req.Offset, false); err != nil {
|
||||
return domain.StoryPublicForwardList{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryPublicForwardList{}, nil
|
||||
}
|
||||
req.ViewerUserID = viewerUserID
|
||||
req.Limit = clampStoryInteractionLimit(req.Limit)
|
||||
return s.stories.ListStoryPublicForwards(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) CanViewStoryStats(ctx context.Context, userID int64, peer domain.Peer) error {
|
||||
return s.authorizeStoryOwner(ctx, userID, peer, storyOwnerEdit)
|
||||
}
|
||||
|
||||
func (s *Service) ListStoryViewerIDs(ctx context.Context, userID int64, owner domain.Peer, storyID, limit int) ([]int64, error) {
|
||||
if err := validateStoryOwner(userID, owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if storyID <= 0 || storyID > domain.MaxStoryID {
|
||||
return nil, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.stories.ListStoryViewerIDs(ctx, owner, storyID, clampStoryPrivacyFanoutLimit(limit))
|
||||
}
|
||||
|
||||
func (s *Service) EditStory(ctx context.Context, userID int64, req domain.StoryEditRequest) (domain.StoryEditResult, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, userID, req.Owner, storyOwnerEdit); err != nil {
|
||||
return domain.StoryEditResult{}, err
|
||||
}
|
||||
if req.ID <= 0 || req.ID > domain.MaxStoryID {
|
||||
return domain.StoryEditResult{}, domain.ErrStoryIDInvalid
|
||||
}
|
||||
if !req.UpdateMedia && !req.UpdateCaption && !req.UpdatePrivacy && !req.UpdateMediaAreas {
|
||||
return domain.StoryEditResult{}, domain.ErrStoryNotModified
|
||||
}
|
||||
if req.UpdateMedia && req.Media == nil {
|
||||
return domain.StoryEditResult{}, domain.ErrStoryNotFound
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryEditResult{}, domain.ErrStoryNotFound
|
||||
}
|
||||
return s.stories.EditStory(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteStories(ctx context.Context, userID int64, peer domain.Peer, ids []int, date int) (domain.StoryMutationResult, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, userID, peer, storyOwnerDelete); err != nil {
|
||||
return domain.StoryMutationResult{}, err
|
||||
}
|
||||
ids, err := normalizeStoryIDsNonEmpty(ids)
|
||||
if err != nil {
|
||||
return domain.StoryMutationResult{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryMutationResult{Peer: peer, IDs: append([]int(nil), ids...)}, nil
|
||||
}
|
||||
return s.stories.DeleteStories(ctx, peer, ids, date)
|
||||
}
|
||||
|
||||
func (s *Service) TogglePinned(ctx context.Context, userID int64, peer domain.Peer, ids []int, pinned bool, date int) (domain.StoryMutationResult, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, userID, peer, storyOwnerPin); err != nil {
|
||||
return domain.StoryMutationResult{}, err
|
||||
}
|
||||
ids, err := normalizeStoryIDs(ids)
|
||||
if err != nil {
|
||||
return domain.StoryMutationResult{}, err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return domain.StoryMutationResult{Peer: peer, IDs: append([]int(nil), ids...)}, nil
|
||||
}
|
||||
return s.stories.TogglePinned(ctx, peer, ids, pinned, date)
|
||||
}
|
||||
|
||||
func (s *Service) TogglePinnedToTop(ctx context.Context, userID int64, peer domain.Peer, ids []int) error {
|
||||
if err := s.authorizeStoryOwner(ctx, userID, peer, storyOwnerPin); err != nil {
|
||||
return err
|
||||
}
|
||||
ids, err := normalizeStoryPinnedToTopIDs(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.stories == nil {
|
||||
return nil
|
||||
}
|
||||
return s.stories.TogglePinnedToTop(ctx, peer, ids)
|
||||
}
|
||||
|
||||
func (s *Service) TogglePeerStoriesHidden(ctx context.Context, viewerUserID int64, peer domain.Peer, hidden bool) error {
|
||||
if s == nil || s.stories == nil || viewerUserID == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.stories.SetPeerHidden(ctx, viewerUserID, peer, hidden)
|
||||
}
|
||||
|
||||
func (s *Service) CanSendStory(ctx context.Context, viewerUserID int64, peer domain.Peer) (int, error) {
|
||||
if err := s.authorizeStoryOwner(ctx, viewerUserID, peer, storyOwnerPost); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return domain.DefaultStoryCanSendRemaining, nil
|
||||
}
|
||||
|
||||
type storyOwnerAction int
|
||||
|
||||
const (
|
||||
storyOwnerPost storyOwnerAction = iota
|
||||
storyOwnerEdit
|
||||
storyOwnerDelete
|
||||
storyOwnerPin
|
||||
)
|
||||
|
||||
func (s *Service) authorizeStoryOwner(ctx context.Context, userID int64, peer domain.Peer, action storyOwnerAction) error {
|
||||
if userID == 0 {
|
||||
return domain.ErrStoryPeerInvalid
|
||||
}
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
if peer.ID == userID {
|
||||
return nil
|
||||
}
|
||||
case domain.PeerTypeChannel:
|
||||
if peer.ID == 0 || s == nil || s.channelAccess == nil {
|
||||
return domain.ErrStoryPeerInvalid
|
||||
}
|
||||
switch action {
|
||||
case storyOwnerPost:
|
||||
return s.channelAccess.CanPostStory(ctx, userID, peer.ID)
|
||||
case storyOwnerEdit:
|
||||
return s.channelAccess.CanEditStory(ctx, userID, peer.ID)
|
||||
case storyOwnerDelete:
|
||||
return s.channelAccess.CanDeleteStory(ctx, userID, peer.ID)
|
||||
case storyOwnerPin:
|
||||
return s.channelAccess.CanPinStory(ctx, userID, peer.ID)
|
||||
}
|
||||
}
|
||||
return domain.ErrStoryPeerInvalid
|
||||
}
|
||||
|
||||
func validateStoryIDs(ids []int) error {
|
||||
if len(ids) > domain.MaxStoryIDs {
|
||||
return domain.ErrStoryIDInvalid
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id <= 0 || id > domain.MaxStoryID {
|
||||
return domain.ErrStoryIDInvalid
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateStoryIDsNonEmpty(ids []int) error {
|
||||
if len(ids) == 0 {
|
||||
return domain.ErrStoryIDInvalid
|
||||
}
|
||||
return validateStoryIDs(ids)
|
||||
}
|
||||
|
||||
func normalizeStoryIDsNonEmpty(ids []int) ([]int, error) {
|
||||
if err := validateStoryIDsNonEmpty(ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalizeStoryIDsUnchecked(ids), nil
|
||||
}
|
||||
|
||||
func normalizeStoryIDs(ids []int) ([]int, error) {
|
||||
if err := validateStoryIDs(ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalizeStoryIDsUnchecked(ids), nil
|
||||
}
|
||||
|
||||
func normalizeStoryPinnedToTopIDs(ids []int) ([]int, error) {
|
||||
ids, err := normalizeStoryIDs(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ids) > domain.MaxStoryPinnedToTop {
|
||||
return nil, domain.ErrStoryIDInvalid
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func normalizeStoryIDsUnchecked(ids []int) []int {
|
||||
seen := make(map[int]struct{}, len(ids))
|
||||
out := make([]int, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateStoryOwner(userID int64, peer domain.Peer) error {
|
||||
if userID == 0 {
|
||||
return domain.ErrStoryPeerInvalid
|
||||
}
|
||||
if peer.Type == domain.PeerTypeUser && peer.ID == userID {
|
||||
return nil
|
||||
}
|
||||
return domain.ErrStoryPeerInvalid
|
||||
}
|
||||
|
||||
func validateStoryListCursor(cursor domain.StoryListCursor) error {
|
||||
if !cursor.Set {
|
||||
return nil
|
||||
}
|
||||
if cursor.Date <= 0 || cursor.Peer.ID <= 0 {
|
||||
return domain.ErrStoryOffsetInvalid
|
||||
}
|
||||
switch cursor.Peer.Type {
|
||||
case domain.PeerTypeUser, domain.PeerTypeChannel:
|
||||
return nil
|
||||
default:
|
||||
return domain.ErrStoryOffsetInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStoryPeriod(period int) int {
|
||||
if period == 0 {
|
||||
return domain.DefaultStoryPeriod
|
||||
}
|
||||
if validStoryPeriod(period) {
|
||||
return period
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func validStoryPeriod(period int) bool {
|
||||
switch period {
|
||||
case 6 * 3600, 12 * 3600, domain.DefaultStoryPeriod, 2 * domain.DefaultStoryPeriod:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func clampStoryLimit(limit int) int {
|
||||
if limit <= 0 || limit > domain.MaxStoryListLimit {
|
||||
return domain.MaxStoryListLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func clampStoryArchiveLimit(limit int) int {
|
||||
if limit == 0 {
|
||||
return 0
|
||||
}
|
||||
if limit < 0 || limit > domain.MaxStoryListLimit {
|
||||
return domain.MaxStoryListLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func clampStoryInteractionLimit(limit int) int {
|
||||
if limit <= 0 || limit > domain.MaxStoryInteractionListLimit {
|
||||
return domain.MaxStoryInteractionListLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func clampStoryPrivacyFanoutLimit(limit int) int {
|
||||
if limit <= 0 || limit > domain.MaxStoryPrivacyFanoutTargets {
|
||||
return domain.MaxStoryPrivacyFanoutTargets
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func normalizeStoryViewQuery(query string) string {
|
||||
query = strings.TrimSpace(query)
|
||||
runes := []rune(query)
|
||||
if len(runes) > domain.MaxStoryViewQueryLength {
|
||||
return string(runes[:domain.MaxStoryViewQueryLength])
|
||||
}
|
||||
return query
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue