owpengram-server/internal/app/account/business.go

511 lines
17 KiB
Go

package account
import (
"context"
"errors"
"fmt"
"strings"
"time"
"unicode/utf8"
"telesrv/internal/domain"
"telesrv/internal/links"
)
const (
maxBusinessLocationAddress = 96
maxBusinessIntroTitle = 64
maxBusinessIntroDesc = 160
)
func (s *Service) GetBusinessProfile(ctx context.Context, userID int64) (domain.BusinessProfile, bool, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.BusinessProfile{UserID: userID}, false, nil
}
return s.business.GetBusinessProfile(ctx, userID)
}
func (s *Service) UpdateBusinessWorkHours(ctx context.Context, userID int64, hours *domain.BusinessWorkHours) (domain.BusinessProfile, error) {
profile, err := s.businessProfileForUpdate(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
normalized, err := normalizeBusinessWorkHours(hours)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.WorkHours = normalized
return s.saveBusinessProfile(ctx, profile)
}
func (s *Service) UpdateBusinessLocation(ctx context.Context, userID int64, location *domain.BusinessLocation) (domain.BusinessProfile, error) {
profile, err := s.businessProfileForUpdate(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
normalized, err := normalizeBusinessLocation(location)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.Location = normalized
return s.saveBusinessProfile(ctx, profile)
}
func (s *Service) UpdateBusinessIntro(ctx context.Context, userID int64, intro *domain.BusinessIntro) (domain.BusinessProfile, error) {
profile, err := s.businessProfileForUpdate(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
normalized, err := normalizeBusinessIntro(intro)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.Intro = normalized
return s.saveBusinessProfile(ctx, profile)
}
func (s *Service) UpdateBusinessGreetingMessage(ctx context.Context, userID int64, greeting *domain.BusinessGreetingMessage) (domain.BusinessProfile, error) {
profile, err := s.businessProfileForUpdate(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
normalized, err := normalizeBusinessGreeting(greeting)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.Greeting = normalized
return s.saveBusinessProfile(ctx, profile)
}
func (s *Service) UpdateBusinessAwayMessage(ctx context.Context, userID int64, away *domain.BusinessAwayMessage) (domain.BusinessProfile, error) {
profile, err := s.businessProfileForUpdate(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
normalized, err := normalizeBusinessAway(away)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.Away = normalized
return s.saveBusinessProfile(ctx, profile)
}
func (s *Service) ListBusinessChatLinks(ctx context.Context, userID int64) ([]domain.BusinessChatLink, error) {
if s == nil || s.business == nil || userID == 0 {
return nil, nil
}
return s.business.ListBusinessChatLinks(ctx, userID)
}
func (s *Service) CreateBusinessChatLink(ctx context.Context, userID int64, input domain.BusinessChatLinkInput) (domain.BusinessChatLink, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.BusinessChatLink{}, domain.ErrPremiumRequired
}
normalized, err := domain.NormalizeBusinessChatLinkInput(input)
if err != nil {
return domain.BusinessChatLink{}, err
}
now := time.Now().Unix()
for i := 0; i < 8; i++ {
slug, err := randomBusinessChatLinkSlug()
if err != nil {
return domain.BusinessChatLink{}, err
}
link, err := s.business.CreateBusinessChatLink(ctx, domain.BusinessChatLink{
OwnerUserID: userID,
Slug: slug,
Link: s.businessChatLinkURL(slug),
Message: normalized.Message,
Entities: normalized.Entities,
Title: normalized.Title,
CreatedAt: now,
UpdatedAt: now,
})
if err == nil {
return link, nil
}
if !errors.Is(err, domain.ErrBusinessChatLinkInvalid) {
return domain.BusinessChatLink{}, err
}
}
return domain.BusinessChatLink{}, domain.ErrBusinessChatLinkInvalid
}
func (s *Service) EditBusinessChatLink(ctx context.Context, userID int64, slug string, input domain.BusinessChatLinkInput) (domain.BusinessChatLink, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.BusinessChatLink{}, domain.ErrPremiumRequired
}
normalized, err := domain.NormalizeBusinessChatLinkInput(input)
if err != nil {
return domain.BusinessChatLink{}, err
}
return s.business.UpdateBusinessChatLink(ctx, userID, strings.TrimSpace(slug), normalized)
}
func (s *Service) DeleteBusinessChatLink(ctx context.Context, userID int64, slug string) (bool, error) {
if s == nil || s.business == nil || userID == 0 {
return false, domain.ErrBusinessChatLinkNotFound
}
return s.business.DeleteBusinessChatLink(ctx, userID, strings.TrimSpace(slug))
}
func (s *Service) ResolveBusinessChatLink(ctx context.Context, slug string, bumpViews bool) (domain.BusinessChatLink, bool, error) {
if s == nil || s.business == nil {
return domain.BusinessChatLink{}, false, nil
}
return s.business.ResolveBusinessChatLink(ctx, strings.TrimSpace(slug), bumpViews)
}
func (s *Service) GetConnectedBusinessBot(ctx context.Context, ownerUserID int64) (domain.ConnectedBusinessBot, bool, error) {
if s == nil || s.business == nil || ownerUserID == 0 {
return domain.ConnectedBusinessBot{}, false, nil
}
return s.business.GetConnectedBusinessBot(ctx, ownerUserID)
}
func (s *Service) SaveConnectedBusinessBot(ctx context.Context, ownerUserID int64, bot domain.ConnectedBusinessBot) (domain.ConnectedBusinessBot, error) {
if s == nil || s.business == nil || ownerUserID == 0 || bot.BotUserID == 0 || bot.BotUserID == ownerUserID {
return domain.ConnectedBusinessBot{}, domain.ErrBotBusinessMissing
}
recipients, err := normalizeBusinessBotRecipients(bot.Recipients)
if err != nil {
return domain.ConnectedBusinessBot{}, err
}
now := time.Now().Unix()
bot.OwnerUserID = ownerUserID
bot.Recipients = recipients
if bot.CreatedAtUnix == 0 {
bot.CreatedAtUnix = now
}
bot.UpdatedAtUnix = now
return s.business.SaveConnectedBusinessBot(ctx, bot)
}
func (s *Service) DeleteConnectedBusinessBot(ctx context.Context, ownerUserID, botUserID int64) (bool, error) {
if s == nil || s.business == nil || ownerUserID == 0 || botUserID == 0 {
return false, domain.ErrBotBusinessMissing
}
return s.business.DeleteConnectedBusinessBot(ctx, ownerUserID, botUserID)
}
func (s *Service) SetConnectedBusinessBotPaused(ctx context.Context, ownerUserID, peerUserID int64, paused bool) (domain.ConnectedBusinessBotPeerState, error) {
if s == nil || s.business == nil || ownerUserID == 0 || peerUserID == 0 || ownerUserID == peerUserID {
return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing
}
if _, ok, err := s.business.GetConnectedBusinessBot(ctx, ownerUserID); err != nil {
return domain.ConnectedBusinessBotPeerState{}, err
} else if !ok {
return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing
}
state, err := s.business.SetConnectedBusinessBotPaused(ctx, ownerUserID, peerUserID, paused)
if err != nil {
return domain.ConnectedBusinessBotPeerState{}, err
}
if state.UpdatedAtUnix == 0 {
state.UpdatedAtUnix = time.Now().Unix()
}
return state, nil
}
func (s *Service) DisableConnectedBusinessBotForPeer(ctx context.Context, ownerUserID, peerUserID int64) (domain.ConnectedBusinessBotPeerState, error) {
if s == nil || s.business == nil || ownerUserID == 0 || peerUserID == 0 || ownerUserID == peerUserID {
return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing
}
if _, ok, err := s.business.GetConnectedBusinessBot(ctx, ownerUserID); err != nil {
return domain.ConnectedBusinessBotPeerState{}, err
} else if !ok {
return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing
}
state, err := s.business.DisableConnectedBusinessBotForPeer(ctx, ownerUserID, peerUserID)
if err != nil {
return domain.ConnectedBusinessBotPeerState{}, err
}
if state.UpdatedAtUnix == 0 {
state.UpdatedAtUnix = time.Now().Unix()
}
return state, nil
}
func (s *Service) GetConnectedBusinessBotPeerState(ctx context.Context, ownerUserID, peerUserID int64) (domain.ConnectedBusinessBotPeerState, bool, error) {
if s == nil || s.business == nil || ownerUserID == 0 || peerUserID == 0 {
return domain.ConnectedBusinessBotPeerState{}, false, nil
}
return s.business.GetConnectedBusinessBotPeerState(ctx, ownerUserID, peerUserID)
}
func (s *Service) ListQuickReplies(ctx context.Context, userID int64) (domain.QuickReplyList, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyList{OwnerUserID: userID}, nil
}
return s.business.ListQuickReplies(ctx, userID, true)
}
func (s *Service) CheckQuickReplyShortcut(ctx context.Context, userID int64, shortcut string) (bool, error) {
if s == nil || s.business == nil || userID == 0 {
if _, err := domain.NormalizeQuickReplyShortcut(shortcut); err != nil {
return false, err
}
return true, nil
}
return s.business.CheckQuickReplyShortcut(ctx, userID, shortcut)
}
func (s *Service) SaveQuickReplyText(ctx context.Context, userID int64, shortcut string, msg domain.QuickReplyMessage) (domain.QuickReplyMutation, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMutation{}, domain.ErrPremiumRequired
}
if msg.Message == "" || utf8.RuneCountInString(msg.Message) > domain.MaxMessageTextLength || len(msg.Entities) > domain.MaxMessageEntityCount {
return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid
}
return s.business.SaveQuickReplyText(ctx, userID, shortcut, msg)
}
func (s *Service) GetQuickReplyMessages(ctx context.Context, userID int64, shortcutID int, ids []int) (domain.QuickReplyMessages, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMessages{}, domain.ErrShortcutInvalid
}
return s.business.GetQuickReplyMessages(ctx, userID, shortcutID, ids)
}
func (s *Service) RenameQuickReplyShortcut(ctx context.Context, userID int64, shortcutID int, shortcut string) (domain.QuickReplyMutation, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMutation{}, domain.ErrPremiumRequired
}
return s.business.RenameQuickReplyShortcut(ctx, userID, shortcutID, shortcut)
}
func (s *Service) ReorderQuickReplies(ctx context.Context, userID int64, order []int) (domain.QuickReplyMutation, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMutation{}, domain.ErrPremiumRequired
}
return s.business.ReorderQuickReplies(ctx, userID, append([]int(nil), order...))
}
func (s *Service) DeleteQuickReplyShortcut(ctx context.Context, userID int64, shortcutID int) (domain.QuickReplyMutation, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid
}
return s.business.DeleteQuickReplyShortcut(ctx, userID, shortcutID)
}
func (s *Service) DeleteQuickReplyMessages(ctx context.Context, userID int64, shortcutID int, ids []int) (domain.QuickReplyMutation, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid
}
return s.business.DeleteQuickReplyMessages(ctx, userID, shortcutID, append([]int(nil), ids...))
}
func (s *Service) businessProfileForUpdate(ctx context.Context, userID int64) (domain.BusinessProfile, error) {
if s == nil || s.business == nil || userID == 0 {
return domain.BusinessProfile{}, domain.ErrPremiumRequired
}
profile, _, err := s.business.GetBusinessProfile(ctx, userID)
if err != nil {
return domain.BusinessProfile{}, err
}
profile.UserID = userID
return profile, nil
}
func (s *Service) saveBusinessProfile(ctx context.Context, profile domain.BusinessProfile) (domain.BusinessProfile, error) {
profile.UpdatedAtUnix = time.Now().Unix()
if err := s.business.SaveBusinessProfile(ctx, profile); err != nil {
return domain.BusinessProfile{}, err
}
return profile, nil
}
func normalizeBusinessWorkHours(in *domain.BusinessWorkHours) (*domain.BusinessWorkHours, error) {
if in == nil {
return nil, nil
}
out := *in
out.TimezoneID = strings.TrimSpace(out.TimezoneID)
out.OpenNow = false
if out.TimezoneID == "" || len(out.WeeklyOpen) == 0 || len(out.WeeklyOpen) > domain.MaxBusinessWorkHourIntervals {
return nil, domain.ErrBusinessProfileInvalid
}
out.WeeklyOpen = append([]domain.BusinessWeeklyOpen(nil), out.WeeklyOpen...)
for _, item := range out.WeeklyOpen {
if item.StartMinute < 0 || item.EndMinute <= item.StartMinute || item.EndMinute > 8*24*60 {
return nil, domain.ErrBusinessProfileInvalid
}
}
return &out, nil
}
func normalizeBusinessLocation(in *domain.BusinessLocation) (*domain.BusinessLocation, error) {
if in == nil {
return nil, nil
}
out := *in
out.Address = strings.TrimSpace(out.Address)
if out.Address == "" || utf8.RuneCountInString(out.Address) > maxBusinessLocationAddress {
return nil, domain.ErrBusinessProfileInvalid
}
if out.Geo != nil {
geo := *out.Geo
if geo.Lat < -90 || geo.Lat > 90 || geo.Long < -180 || geo.Long > 180 {
return nil, domain.ErrBusinessProfileInvalid
}
out.Geo = &geo
}
return &out, nil
}
func normalizeBusinessIntro(in *domain.BusinessIntro) (*domain.BusinessIntro, error) {
if in == nil {
return nil, nil
}
out := *in
out.Title = strings.TrimSpace(out.Title)
out.Description = strings.TrimSpace(out.Description)
if out.Title == "" && out.Description == "" && out.StickerDocumentID == 0 {
return nil, nil
}
if utf8.RuneCountInString(out.Title) > maxBusinessIntroTitle || utf8.RuneCountInString(out.Description) > maxBusinessIntroDesc {
return nil, domain.ErrBusinessProfileInvalid
}
return &out, nil
}
func normalizeBusinessGreeting(in *domain.BusinessGreetingMessage) (*domain.BusinessGreetingMessage, error) {
if in == nil {
return nil, nil
}
out := *in
if out.ShortcutID <= 0 || !validGreetingNoActivityDays(out.NoActivityDays) {
return nil, domain.ErrBusinessProfileInvalid
}
recipients, err := normalizeBusinessRecipients(out.Recipients)
if err != nil {
return nil, err
}
out.Recipients = recipients
return &out, nil
}
func normalizeBusinessAway(in *domain.BusinessAwayMessage) (*domain.BusinessAwayMessage, error) {
if in == nil {
return nil, nil
}
out := *in
if out.ShortcutID <= 0 {
return nil, domain.ErrBusinessProfileInvalid
}
switch out.Schedule.Kind {
case domain.BusinessAwayScheduleAlways, domain.BusinessAwayScheduleOutsideWorkHours:
out.Schedule.StartDate = 0
out.Schedule.EndDate = 0
case domain.BusinessAwayScheduleCustom:
if out.Schedule.StartDate <= 0 || out.Schedule.EndDate <= out.Schedule.StartDate {
return nil, domain.ErrBusinessProfileInvalid
}
default:
return nil, domain.ErrBusinessProfileInvalid
}
recipients, err := normalizeBusinessRecipients(out.Recipients)
if err != nil {
return nil, err
}
out.Recipients = recipients
return &out, nil
}
func normalizeBusinessRecipients(in domain.BusinessRecipients) (domain.BusinessRecipients, error) {
out := in
out.Users = append([]int64(nil), in.Users...)
if len(out.Users) > domain.MaxBusinessRecipientUsers {
return domain.BusinessRecipients{}, domain.ErrBusinessProfileInvalid
}
seen := make(map[int64]struct{}, len(out.Users))
users := out.Users[:0]
for _, id := range out.Users {
if id <= 0 {
return domain.BusinessRecipients{}, domain.ErrBusinessProfileInvalid
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
users = append(users, id)
}
out.Users = users
if !out.ExistingChats && !out.NewChats && !out.Contacts && !out.NonContacts && len(out.Users) == 0 {
return domain.BusinessRecipients{}, domain.ErrBusinessProfileInvalid
}
return out, nil
}
func normalizeBusinessBotRecipients(in domain.BusinessBotRecipients) (domain.BusinessBotRecipients, error) {
out := in
users, err := dedupeBusinessUserIDs(in.Users)
if err != nil {
return domain.BusinessBotRecipients{}, err
}
excluded, err := dedupeBusinessUserIDs(in.ExcludeUsers)
if err != nil {
return domain.BusinessBotRecipients{}, err
}
if len(users)+len(excluded) > domain.MaxBusinessRecipientUsers {
return domain.BusinessBotRecipients{}, domain.ErrBusinessProfileInvalid
}
if out.ExcludeSelected {
merged := append(users, excluded...)
users, err = dedupeBusinessUserIDs(merged)
if err != nil {
return domain.BusinessBotRecipients{}, err
}
excluded = nil
}
out.Users = users
out.ExcludeUsers = excluded
if !out.ExcludeSelected && !out.ExistingChats && !out.NewChats && !out.Contacts && !out.NonContacts && len(out.Users) == 0 {
return domain.BusinessBotRecipients{}, domain.ErrBusinessRecipientsEmpty
}
return out, nil
}
func dedupeBusinessUserIDs(in []int64) ([]int64, error) {
if len(in) == 0 {
return nil, nil
}
seen := make(map[int64]struct{}, len(in))
out := make([]int64, 0, len(in))
for _, id := range in {
if id <= 0 {
return nil, domain.ErrBusinessProfileInvalid
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out, nil
}
func validGreetingNoActivityDays(days int) bool {
switch days {
case 7, 14, 21, 28:
return true
default:
return false
}
}
func randomBusinessChatLinkSlug() (string, error) {
value, err := randomInt64()
if err != nil {
return "", err
}
return fmt.Sprintf("%x", value), nil
}
func (s *Service) businessChatLinkURL(slug string) string {
baseURL := links.DefaultPublicBaseURL
if s != nil && s.publicBaseURL != "" {
baseURL = s.publicBaseURL
}
return links.Build(baseURL, "m/"+slug, nil)
}