package rpc import ( "context" "errors" "strings" "github.com/iamxvbaba/td/tg" "telesrv/internal/domain" ) func (r *Router) accountBusinessAutomation() (AccountBusinessAutomationService, bool) { if r.deps.Account == nil { return nil, false } svc, ok := r.deps.Account.(AccountBusinessAutomationService) return svc, ok } func (r *Router) onAccountUpdateBusinessWorkHours(ctx context.Context, req *tg.AccountUpdateBusinessWorkHoursRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, premiumAccountRequiredErr() } hours, err := domainBusinessWorkHours(req) if err != nil { return false, err } if _, err := svc.UpdateBusinessWorkHours(ctx, userID, hours); err != nil { return false, businessAutomationErr(err) } r.invalidateRPCProjectionForUser(userID) return true, nil } func (r *Router) onAccountUpdateBusinessLocation(ctx context.Context, req *tg.AccountUpdateBusinessLocationRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, premiumAccountRequiredErr() } location, err := domainBusinessLocation(req) if err != nil { return false, err } if _, err := svc.UpdateBusinessLocation(ctx, userID, location); err != nil { return false, businessAutomationErr(err) } r.invalidateRPCProjectionForUser(userID) return true, nil } func (r *Router) onAccountUpdateBusinessIntro(ctx context.Context, req *tg.AccountUpdateBusinessIntroRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, premiumAccountRequiredErr() } intro, err := domainBusinessIntro(req) if err != nil { return false, err } if _, err := svc.UpdateBusinessIntro(ctx, userID, intro); err != nil { return false, businessAutomationErr(err) } r.invalidateRPCProjectionForUser(userID) return true, nil } func (r *Router) onAccountUpdateBusinessGreetingMessage(ctx context.Context, req *tg.AccountUpdateBusinessGreetingMessageRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, premiumAccountRequiredErr() } greeting, err := r.domainBusinessGreeting(ctx, userID, req) if err != nil { return false, err } if _, err := svc.UpdateBusinessGreetingMessage(ctx, userID, greeting); err != nil { return false, businessAutomationErr(err) } r.invalidateRPCProjectionForUser(userID) return true, nil } func (r *Router) onAccountUpdateBusinessAwayMessage(ctx context.Context, req *tg.AccountUpdateBusinessAwayMessageRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, premiumAccountRequiredErr() } away, err := r.domainBusinessAway(ctx, userID, req) if err != nil { return false, err } if _, err := svc.UpdateBusinessAwayMessage(ctx, userID, away); err != nil { return false, businessAutomationErr(err) } r.invalidateRPCProjectionForUser(userID) return true, nil } func (r *Router) onAccountGetBusinessChatLinks(ctx context.Context) (*tg.AccountBusinessChatLinks, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return &tg.AccountBusinessChatLinks{ Links: []tg.BusinessChatLink{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}, }, nil } links, err := svc.ListBusinessChatLinks(ctx, userID) if err != nil { return nil, internalErr() } return &tg.AccountBusinessChatLinks{ Links: tgBusinessChatLinks(links), Chats: []tg.ChatClass{}, Users: []tg.UserClass{}, }, nil } func (r *Router) onAccountCreateBusinessChatLink(ctx context.Context, link tg.InputBusinessChatLink) (*tg.BusinessChatLink, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return nil, premiumAccountRequiredErr() } input, err := domainBusinessChatLinkInput(link) if err != nil { return nil, err } created, err := svc.CreateBusinessChatLink(ctx, userID, input) if err != nil { return nil, businessAutomationErr(err) } out := tgBusinessChatLink(created) return &out, nil } func (r *Router) onAccountEditBusinessChatLink(ctx context.Context, req *tg.AccountEditBusinessChatLinkRequest) (*tg.BusinessChatLink, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } if req == nil || strings.TrimSpace(req.Slug) == "" { return nil, chatlinkSlugEmptyErr() } svc, ok := r.accountBusinessAutomation() if !ok { return nil, premiumAccountRequiredErr() } input, err := domainBusinessChatLinkInput(req.Link) if err != nil { return nil, err } updated, err := svc.EditBusinessChatLink(ctx, userID, req.Slug, input) if err != nil { return nil, businessAutomationErr(err) } out := tgBusinessChatLink(updated) return &out, nil } func (r *Router) onAccountDeleteBusinessChatLink(ctx context.Context, slug string) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } if strings.TrimSpace(slug) == "" { return false, chatlinkSlugEmptyErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, chatlinkSlugExpiredErr() } deleted, err := svc.DeleteBusinessChatLink(ctx, userID, slug) if err != nil { return false, businessAutomationErr(err) } if !deleted { return false, chatlinkSlugExpiredErr() } return true, nil } func (r *Router) onAccountResolveBusinessChatLink(ctx context.Context, slug string) (*tg.AccountResolvedBusinessChatLinks, error) { if strings.TrimSpace(slug) == "" { return nil, chatlinkSlugEmptyErr() } svc, ok := r.accountBusinessAutomation() if !ok { return nil, chatlinkSlugExpiredErr() } link, found, err := svc.ResolveBusinessChatLink(ctx, slug, true) if err != nil { return nil, internalErr() } if !found { return nil, chatlinkSlugExpiredErr() } viewerID, _, _ := r.currentUserID(ctx) users := []tg.UserClass{} if r.deps.Users != nil { user, found, err := r.deps.Users.ByID(ctx, viewerID, link.OwnerUserID) if err != nil { return nil, internalErr() } if found { if viewerID != 0 && user.ID == viewerID { users = append(users, r.tgSelfUser(user)) } else { users = append(users, r.tgUser(user)) } } } r.applyPeerReadModels(ctx, viewerID, users, nil) return &tg.AccountResolvedBusinessChatLinks{ Peer: &tg.PeerUser{UserID: link.OwnerUserID}, Message: link.Message, Entities: tgMessageEntities(link.Entities), Chats: []tg.ChatClass{}, Users: users, }, nil } func (r *Router) onAccountGetConnectedBots(ctx context.Context) (*tg.AccountConnectedBots, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return &tg.AccountConnectedBots{ConnectedBots: []tg.ConnectedBot{}, Users: []tg.UserClass{}}, nil } bot, found, err := svc.GetConnectedBusinessBot(ctx, userID) if err != nil { return nil, internalErr() } if !found || bot.BotUserID == 0 { return &tg.AccountConnectedBots{ConnectedBots: []tg.ConnectedBot{}, Users: []tg.UserClass{}}, nil } botUser, found, err := r.connectedBusinessBotByID(ctx, userID, bot.BotUserID) if err != nil { return nil, err } if !found { return &tg.AccountConnectedBots{ConnectedBots: []tg.ConnectedBot{}, Users: []tg.UserClass{}}, nil } out := &tg.AccountConnectedBots{ ConnectedBots: []tg.ConnectedBot{tgConnectedBot(bot)}, Users: []tg.UserClass{r.tgUser(botUser)}, } r.applyPeerReadModels(ctx, userID, out.Users, nil) return out, nil } func (r *Router) onAccountUpdateConnectedBot(ctx context.Context, req *tg.AccountUpdateConnectedBotRequest) (tg.UpdatesClass, error) { if req == nil { return nil, inputRequestInvalidErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } svc, ok := r.accountBusinessAutomation() if !ok { return nil, premiumAccountRequiredErr() } botUser, found, err := r.connectedBusinessBotFromInput(ctx, userID, req.Bot) if err != nil { return nil, err } if !found { return nil, botBusinessMissingErr() } if req.Deleted { if _, err := svc.DeleteConnectedBusinessBot(ctx, userID, botUser.ID); err != nil { return nil, businessAutomationErr(err) } r.invalidateRPCProjectionForViewer(userID) return r.connectedBusinessBotEmptyUpdates(ctx, userID, botUser), nil } recipients, err := r.domainBusinessBotRecipients(ctx, userID, req.Recipients) if err != nil { return nil, err } saved, err := svc.SaveConnectedBusinessBot(ctx, userID, domain.ConnectedBusinessBot{ BotUserID: botUser.ID, Recipients: recipients, Rights: domainBusinessBotRightsForUpdate(req), }) if err != nil { return nil, businessAutomationErr(err) } r.invalidateRPCProjectionForViewer(userID) return r.connectedBusinessBotEmptyUpdates(ctx, userID, botUser, tgConnectedBot(saved)), nil } func (r *Router) onAccountToggleConnectedBotPaused(ctx context.Context, req *tg.AccountToggleConnectedBotPausedRequest) (bool, error) { if req == nil { return false, inputRequestInvalidErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer) if err != nil { return false, err } if peer.Type != domain.PeerTypeUser || peer.ID == userID { return false, peerIDInvalidErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, botBusinessMissingErr() } if _, err := svc.SetConnectedBusinessBotPaused(ctx, userID, peer.ID, req.Paused); err != nil { return false, businessAutomationErr(err) } settings, err := r.connectedBusinessBotPeerSettings(ctx, userID, peer, domain.PeerSettings{}) if err != nil { return false, err } if err := r.recordConnectedBusinessPeerSettings(ctx, userID, peer, settings); err != nil { return false, internalErr() } r.invalidateRPCProjectionForPeer(userID, peer) return true, nil } func (r *Router) onAccountDisablePeerConnectedBot(ctx context.Context, input tg.InputPeerClass) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input) if err != nil { return false, err } if peer.Type != domain.PeerTypeUser || peer.ID == userID { return false, peerIDInvalidErr() } svc, ok := r.accountBusinessAutomation() if !ok { return false, botBusinessMissingErr() } if _, err := svc.DisableConnectedBusinessBotForPeer(ctx, userID, peer.ID); err != nil { return false, businessAutomationErr(err) } if err := r.recordConnectedBusinessPeerSettings(ctx, userID, peer, domain.PeerSettings{}); err != nil { return false, internalErr() } r.invalidateRPCProjectionForPeer(userID, peer) return true, nil } func (r *Router) connectedBusinessBotByID(ctx context.Context, currentUserID, botUserID int64) (domain.User, bool, error) { if r.deps.Users == nil || botUserID == 0 { return domain.User{}, false, nil } u, found, err := r.deps.Users.ByID(ctx, currentUserID, botUserID) if err != nil { return domain.User{}, false, internalErr() } if !found || !connectedBusinessBotUsable(u) { return domain.User{}, false, nil } return u, true, nil } func (r *Router) connectedBusinessBotFromInput(ctx context.Context, currentUserID int64, input tg.InputUserClass) (domain.User, bool, error) { if r.deps.Users == nil { return domain.User{}, false, internalErr() } u, found, err := r.userFromInput(ctx, currentUserID, input) if err != nil { return domain.User{}, false, internalErr() } if !found || !connectedBusinessBotUsable(u) { return domain.User{}, false, nil } return u, true, nil } func connectedBusinessBotUsable(u domain.User) bool { return u.Bot && u.ID != 0 && u.ID != domain.BotFatherUserID } func (r *Router) connectedBusinessBotEmptyUpdates(ctx context.Context, viewerUserID int64, botUser domain.User, bots ...tg.ConnectedBot) *tg.Updates { users := []tg.UserClass{} if botUser.ID != 0 { users = append(users, r.tgUser(botUser)) } out := &tg.Updates{ Updates: []tg.UpdateClass{}, Users: users, Chats: []tg.ChatClass{}, Date: int(r.clock.Now().Unix()), } r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats) return out } func (r *Router) connectedBusinessBotPeerSettings(ctx context.Context, ownerUserID int64, peer domain.Peer, settings domain.PeerSettings) (domain.PeerSettings, error) { if peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == ownerUserID { return settings, nil } svc, ok := r.accountBusinessAutomation() if !ok { return settings, nil } bot, found, err := svc.GetConnectedBusinessBot(ctx, ownerUserID) if err != nil { return domain.PeerSettings{}, internalErr() } if !found || bot.BotUserID == 0 { return settings, nil } state, stateFound, err := svc.GetConnectedBusinessBotPeerState(ctx, ownerUserID, peer.ID) if err != nil { return domain.PeerSettings{}, internalErr() } if stateFound && state.Disabled { return settings, nil } existingChat, isContact := r.connectedBusinessPeerFacts(ctx, ownerUserID, peer.ID) if !domain.BusinessBotRecipientsMatch(bot.Recipients, existingChat, isContact, peer.ID) { return settings, nil } settings.BusinessBotID = bot.BotUserID settings.BusinessBotPaused = stateFound && state.Paused settings.BusinessBotCanReply = bot.Rights.Reply && !settings.BusinessBotPaused if botUser, found, err := r.connectedBusinessBotByID(ctx, ownerUserID, bot.BotUserID); err != nil { return domain.PeerSettings{}, err } else if found { settings.BusinessBotManageURL = r.connectedBusinessBotManageURL(botUser) } if settings.BusinessBotManageURL == "" { settings.BusinessBotManageURL = r.publicAppLink("business-bot") } return settings, nil } func (r *Router) connectedBusinessPeerFacts(ctx context.Context, ownerUserID, peerUserID int64) (existingChat, isContact bool) { if r.deps.Dialogs != nil { list, err := r.deps.Dialogs.GetPeerDialogs(ctx, ownerUserID, []domain.Peer{{Type: domain.PeerTypeUser, ID: peerUserID}}) if err == nil && len(list.Dialogs) > 0 && list.Dialogs[0].TopMessage > 0 { existingChat = true } } if r.deps.Contacts != nil { settings, err := r.deps.Contacts.GetPeerSettings(ctx, ownerUserID, domain.Peer{Type: domain.PeerTypeUser, ID: peerUserID}) isContact = err == nil && !settings.AddContact } return existingChat, isContact } func (r *Router) connectedBusinessBotManageURL(bot domain.User) string { if bot.Username == "" { return "" } return r.publicLink(bot.Username) } func (r *Router) recordConnectedBusinessPeerSettings(ctx context.Context, userID int64, peer domain.Peer, settings domain.PeerSettings) error { if r.deps.Updates == nil || userID == 0 { return nil } authKeyID, _ := AuthKeyIDFrom(ctx) sessionID, _ := SessionIDFrom(ctx) event, _, err := r.deps.Updates.RecordPeerSettings(ctx, authKeyID, userID, peer, settings, rawAuthKeyIDForOrigin(ctx), sessionID) if err != nil { return err } if sessionID != 0 { r.bookkeepAuxPtsForCurrentSession(ctx, event) } r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event)) return nil } func premiumAccountRequiredErr() error { return tgerr400("PREMIUM_ACCOUNT_REQUIRED") } func botBusinessMissingErr() error { return tgerr400("BOT_BUSINESS_MISSING") } func botNotConnectedYetErr() error { return tgerr400("BOT_NOT_CONNECTED_YET") } func botAlreadyDisabledErr() error { return tgerr400("BOT_ALREADY_DISABLED") } func chatlinkSlugEmptyErr() error { return tgerr400("CHATLINK_SLUG_EMPTY") } func chatlinkSlugExpiredErr() error { return tgerr400("CHATLINK_SLUG_EXPIRED") } func businessAutomationErr(err error) error { switch { case errors.Is(err, domain.ErrPremiumRequired): return premiumAccountRequiredErr() case errors.Is(err, domain.ErrBusinessProfileInvalid): return inputRequestInvalidErr() case errors.Is(err, domain.ErrBusinessRecipientsEmpty): return tgerr400("BUSINESS_RECIPIENTS_EMPTY") case errors.Is(err, domain.ErrBotBusinessMissing): return botBusinessMissingErr() case errors.Is(err, domain.ErrBotNotConnectedYet): return botNotConnectedYetErr() case errors.Is(err, domain.ErrBotAlreadyDisabled): return botAlreadyDisabledErr() case errors.Is(err, domain.ErrBusinessChatLinkInvalid): return inputRequestInvalidErr() case errors.Is(err, domain.ErrBusinessChatLinkNotFound): return chatlinkSlugExpiredErr() case errors.Is(err, domain.ErrBusinessChatLinksTooMuch): return tgerr400("CHATLINKS_TOO_MUCH") case errors.Is(err, domain.ErrShortcutInvalid): return shortcutInvalidErr() case errors.Is(err, domain.ErrShortcutOccupied): return tgerr400("SHORTCUT_OCCUPIED") case errors.Is(err, domain.ErrQuickRepliesTooMuch): return tgerr400("SHORTCUTS_TOO_MUCH") default: return internalErr() } } func (r *Router) applyBusinessProfileToUserFull(ctx context.Context, full *tg.UserFull, profile domain.BusinessProfile) { if full == nil { return } if hours, ok := tgBusinessWorkHours(profile.WorkHours); ok { full.SetBusinessWorkHours(hours) } if location, ok := tgBusinessLocation(profile.Location); ok { full.SetBusinessLocation(location) } if intro, ok := r.tgBusinessIntro(ctx, profile.Intro); ok { full.SetBusinessIntro(intro) } if greeting, ok := tgBusinessGreeting(profile.Greeting); ok { full.SetBusinessGreetingMessage(greeting) } if away, ok := tgBusinessAway(profile.Away); ok { full.SetBusinessAwayMessage(away) } }