feat: sync chatlist sharing support
This commit is contained in:
parent
ec6e8fd13d
commit
2a9c12263f
62 changed files with 3408 additions and 212 deletions
|
|
@ -455,7 +455,7 @@ func (r *Router) connectedBusinessBotPeerSettings(ctx context.Context, ownerUser
|
|||
if botUser, found, err := r.connectedBusinessBotByID(ctx, ownerUserID, bot.BotUserID); err != nil {
|
||||
return domain.PeerSettings{}, err
|
||||
} else if found {
|
||||
settings.BusinessBotManageURL = connectedBusinessBotManageURL(botUser)
|
||||
settings.BusinessBotManageURL = r.connectedBusinessBotManageURL(botUser)
|
||||
}
|
||||
if settings.BusinessBotManageURL == "" {
|
||||
settings.BusinessBotManageURL = "telesrv://business-bot"
|
||||
|
|
@ -477,11 +477,11 @@ func (r *Router) connectedBusinessPeerFacts(ctx context.Context, ownerUserID, pe
|
|||
return existingChat, isContact
|
||||
}
|
||||
|
||||
func connectedBusinessBotManageURL(bot domain.User) string {
|
||||
func (r *Router) connectedBusinessBotManageURL(bot domain.User) string {
|
||||
if bot.Username == "" {
|
||||
return ""
|
||||
}
|
||||
return "https://telesrv.net/" + bot.Username
|
||||
return r.publicLink(bot.Username)
|
||||
}
|
||||
|
||||
func (r *Router) recordConnectedBusinessPeerSettings(ctx context.Context, userID int64, peer domain.Peer, settings domain.PeerSettings) error {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ func (r *Router) onAccountInstallTheme(ctx context.Context, req *tg.AccountInsta
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// onAccountGetTheme 按 id 或 slug(深链 telesrv.net/addtheme/<slug>)取主题。
|
||||
// onAccountGetTheme 按 id 或 slug(公开 addtheme 深链)取主题。
|
||||
func (r *Router) onAccountGetTheme(ctx context.Context, req *tg.AccountGetThemeRequest) (*tg.Theme, error) {
|
||||
if req == nil {
|
||||
return nil, themeInvalidErr()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
const aiComposeToneWebPageType = "telegram_aicomposetone"
|
||||
|
||||
func (r *Router) resolveAIComposeStyleWebPage(ctx context.Context, rawURL string) (domain.MessageWebPage, bool) {
|
||||
link, ok := parseAIComposeStyleLink(rawURL)
|
||||
link, ok := parseAIComposeStyleLink(rawURL, r.publicLinkHost())
|
||||
if !ok || r.deps.AICompose == nil {
|
||||
return domain.MessageWebPage{}, false
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ type aiComposeStyleLink struct {
|
|||
slug string
|
||||
}
|
||||
|
||||
func parseAIComposeStyleLink(raw string) (aiComposeStyleLink, bool) {
|
||||
func parseAIComposeStyleLink(raw, publicHost string) (aiComposeStyleLink, bool) {
|
||||
normalized, ok := domain.NormalizeWebPageURL(raw)
|
||||
if !ok {
|
||||
return aiComposeStyleLink{}, false
|
||||
|
|
@ -72,7 +72,7 @@ func parseAIComposeStyleLink(raw string) (aiComposeStyleLink, bool) {
|
|||
return aiComposeStyleLink{}, false
|
||||
}
|
||||
host := strings.ToLower(u.Hostname())
|
||||
if !aiComposeStyleHostAllowed(host) {
|
||||
if !aiComposeStyleHostAllowed(host, publicHost) {
|
||||
return aiComposeStyleLink{}, false
|
||||
}
|
||||
parts := strings.Split(strings.Trim(strings.ToLower(u.EscapedPath()), "/"), "/")
|
||||
|
|
@ -96,7 +96,10 @@ func parseAIComposeStyleLink(raw string) (aiComposeStyleLink, bool) {
|
|||
return aiComposeStyleLink{normalized: normalized, display: display, slug: slug}, true
|
||||
}
|
||||
|
||||
func aiComposeStyleHostAllowed(host string) bool {
|
||||
func aiComposeStyleHostAllowed(host, publicHost string) bool {
|
||||
if publicHost != "" && strings.EqualFold(host, publicHost) {
|
||||
return true
|
||||
}
|
||||
switch host {
|
||||
case "t.me", "telegram.me", "telesrv.net", "localhost", "127.0.0.1":
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func startParamInvalidErr() error { return tgerr.New(400, "START_PARAM_INVALID") }
|
||||
|
||||
// onMessagesStartBot 处理 messages.startBot(深链 telesrv.net/bot?start=payload 与「启动 bot」
|
||||
// onMessagesStartBot 处理 messages.startBot(公开 bot?start 深链与「启动 bot」
|
||||
// 入口)。语义:向 bot 发送一条可见的 "/start" 或 "/start <param>" 普通私聊消息,走标准
|
||||
// SendPrivateText 双盒+outbox,返回真实 Updates(I7)。bot 经此收到 start_param。
|
||||
// P3 仅私聊启动;peer 为群(加 bot 进群)后移(P4,群内 bot)。
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputCha
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
full := tgChannelFull(view)
|
||||
full := tgChannelFull(view, r.cfg.PublicBaseURL)
|
||||
r.applyStarGiftsCountToChannelFull(ctx, view.Channel.ID, full)
|
||||
userIDs := []int64{view.Channel.CreatorUserID, view.Self.UserID}
|
||||
// 注:Bots 过滤实际会返回群内 bot(TestGroupBotRPCShape 覆盖),这里据此富化 full.BotInfo。
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ func (r *Router) onMessagesExportChatInvite(ctx context.Context, req *tg.Message
|
|||
return nil, channelInviteErr(err)
|
||||
}
|
||||
r.invalidateRPCProjectionForChannel(res.Channel.ID)
|
||||
return tgExportedChannelInvite(res.Invite), nil
|
||||
return r.tgExportedChannelInvite(res.Invite), nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesCheckChatInvite(ctx context.Context, hash string) (tg.ChatInviteClass, error) {
|
||||
|
|
@ -141,7 +143,7 @@ func (r *Router) onMessagesGetExportedChatInvites(ctx context.Context, req *tg.M
|
|||
userIDs := []int64{adminID}
|
||||
invites := make([]tg.ExportedChatInviteClass, 0, len(list.Invites))
|
||||
for _, invite := range list.Invites {
|
||||
invites = append(invites, tgExportedChannelInvite(invite))
|
||||
invites = append(invites, r.tgExportedChannelInvite(invite))
|
||||
userIDs = append(userIDs, invite.AdminUserID)
|
||||
}
|
||||
return &tg.MessagesExportedChatInvites{
|
||||
|
|
@ -169,7 +171,7 @@ func (r *Router) onMessagesGetExportedChatInvite(ctx context.Context, req *tg.Me
|
|||
return nil, channelInviteErr(err)
|
||||
}
|
||||
return &tg.MessagesExportedChatInvite{
|
||||
Invite: tgExportedChannelInvite(invite),
|
||||
Invite: r.tgExportedChannelInvite(invite),
|
||||
Users: r.tgUsersForIDs(ctx, userID, []int64{invite.AdminUserID}),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -212,12 +214,12 @@ func (r *Router) onMessagesEditExportedChatInvite(ctx context.Context, req *tg.M
|
|||
users := r.tgUsersForIDs(ctx, userID, []int64{edited.Invite.AdminUserID})
|
||||
if edited.NewInvite != nil {
|
||||
return &tg.MessagesExportedChatInviteReplaced{
|
||||
Invite: tgExportedChannelInvite(edited.Invite),
|
||||
NewInvite: tgExportedChannelInvite(*edited.NewInvite),
|
||||
Invite: r.tgExportedChannelInvite(edited.Invite),
|
||||
NewInvite: r.tgExportedChannelInvite(*edited.NewInvite),
|
||||
Users: users,
|
||||
}, nil
|
||||
}
|
||||
return &tg.MessagesExportedChatInvite{Invite: tgExportedChannelInvite(edited.Invite), Users: users}, nil
|
||||
return &tg.MessagesExportedChatInvite{Invite: r.tgExportedChannelInvite(edited.Invite), Users: users}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesDeleteRevokedExportedChatInvites(ctx context.Context, req *tg.MessagesDeleteRevokedExportedChatInvitesRequest) (bool, error) {
|
||||
|
|
@ -353,12 +355,16 @@ func createChatInviteMemberIDs(ids []int64, selfUserID int64) []int64 {
|
|||
return out
|
||||
}
|
||||
|
||||
func tgExportedChannelInvite(invite domain.ChannelInvite) tg.ExportedChatInviteClass {
|
||||
func (r *Router) tgExportedChannelInvite(invite domain.ChannelInvite) tg.ExportedChatInviteClass {
|
||||
return tgExportedChannelInvite(invite, r.cfg.PublicBaseURL)
|
||||
}
|
||||
|
||||
func tgExportedChannelInvite(invite domain.ChannelInvite, publicBaseURL string) tg.ExportedChatInviteClass {
|
||||
out := &tg.ChatInviteExported{
|
||||
Revoked: invite.Revoked,
|
||||
Permanent: invite.Permanent,
|
||||
RequestNeeded: invite.RequestNeeded,
|
||||
Link: "https://telesrv.net/+" + invite.Hash,
|
||||
Link: publicLinkWithBaseURL(publicBaseURL, "+"+invite.Hash),
|
||||
AdminID: invite.AdminUserID,
|
||||
Date: invite.Date,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/gotd/td/tg"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"telesrv/internal/domain"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) onChannelsExportMessageLink(ctx context.Context, req *tg.ChannelsExportMessageLinkRequest) (*tg.ExportedMessageLink, error) {
|
||||
|
|
@ -28,9 +30,9 @@ func (r *Router) onChannelsExportMessageLink(ctx context.Context, req *tg.Channe
|
|||
}
|
||||
link := ""
|
||||
if view.Channel.Username != "" {
|
||||
link = "https://telesrv.net/" + view.Channel.Username + "/" + strconv.Itoa(req.ID)
|
||||
link = r.publicLink(view.Channel.Username + "/" + strconv.Itoa(req.ID))
|
||||
} else {
|
||||
link = "https://telesrv.net/c/" + strconv.FormatInt(view.Channel.ID, 10) + "/" + strconv.Itoa(req.ID)
|
||||
link = r.publicLink("c/" + strconv.FormatInt(view.Channel.ID, 10) + "/" + strconv.Itoa(req.ID))
|
||||
}
|
||||
if req.Thread {
|
||||
if rootID := channelMessageThreadRootID(history.Messages[0]); rootID > 0 && rootID != req.ID {
|
||||
|
|
|
|||
526
internal/rpc/chatlists.go
Normal file
526
internal/rpc/chatlists.go
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) registerChatlists(d *tg.ServerDispatcher) {
|
||||
d.OnChatlistsExportChatlistInvite(r.onChatlistsExportChatlistInvite)
|
||||
d.OnChatlistsDeleteExportedInvite(r.onChatlistsDeleteExportedInvite)
|
||||
d.OnChatlistsEditExportedInvite(r.onChatlistsEditExportedInvite)
|
||||
d.OnChatlistsGetExportedInvites(r.onChatlistsGetExportedInvites)
|
||||
d.OnChatlistsCheckChatlistInvite(r.onChatlistsCheckChatlistInvite)
|
||||
d.OnChatlistsJoinChatlistInvite(r.onChatlistsJoinChatlistInvite)
|
||||
d.OnChatlistsGetChatlistUpdates(r.onChatlistsGetChatlistUpdates)
|
||||
d.OnChatlistsJoinChatlistUpdates(r.onChatlistsJoinChatlistUpdates)
|
||||
d.OnChatlistsHideChatlistUpdates(r.onChatlistsHideChatlistUpdates)
|
||||
d.OnChatlistsGetLeaveChatlistSuggestions(r.onChatlistsGetLeaveChatlistSuggestions)
|
||||
d.OnChatlistsLeaveChatlist(r.onChatlistsLeaveChatlist)
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsExportChatlistInvite(ctx context.Context, req *tg.ChatlistsExportChatlistInviteRequest) (*tg.ChatlistsExportedChatlistInvite, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folder, invite, err := r.deps.Chatlists.ExportInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Title, peers, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
if err := r.recordChatlistFilterUpdate(ctx, userID, folder.ID, &folder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tg.ChatlistsExportedChatlistInvite{
|
||||
Filter: tgDialogFilter(folder),
|
||||
Invite: *r.tgExportedChatlistInvite(invite),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsDeleteExportedInvite(ctx context.Context, req *tg.ChatlistsDeleteExportedInviteRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
folder, changed, err := r.deps.Chatlists.DeleteInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Slug)
|
||||
if err != nil {
|
||||
return false, chatlistErr(err)
|
||||
}
|
||||
if changed {
|
||||
if err := r.recordChatlistFilterUpdate(ctx, userID, folder.ID, &folder); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsEditExportedInvite(ctx context.Context, req *tg.ChatlistsEditExportedInviteRequest) (*tg.ExportedChatlistInvite, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
var title *string
|
||||
if v, ok := req.GetTitle(); ok {
|
||||
title = &v
|
||||
}
|
||||
var peersPtr *[]domain.DialogFolderPeer
|
||||
if v, ok := req.GetPeers(); ok {
|
||||
peers, err := r.dialogFolderPeersFromInput(ctx, userID, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peersPtr = &peers
|
||||
}
|
||||
invite, err := r.deps.Chatlists.EditInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Slug, title, peersPtr, req.Flags.Has(0))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
return r.tgExportedChatlistInvite(invite), nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsGetExportedInvites(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (*tg.ChatlistsExportedInvites, error) {
|
||||
if r.deps.Chatlists == nil {
|
||||
return &tg.ChatlistsExportedInvites{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
invites, err := r.deps.Chatlists.ListInvites(ctx, userID, chatlistFilterID(chatlist))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
out := &tg.ChatlistsExportedInvites{
|
||||
Invites: make([]tg.ExportedChatlistInvite, 0, len(invites)),
|
||||
}
|
||||
allPeers := make([]domain.DialogFolderPeer, 0)
|
||||
for _, invite := range invites {
|
||||
out.Invites = append(out.Invites, *r.tgExportedChatlistInvite(invite))
|
||||
allPeers = append(allPeers, invite.Peers...)
|
||||
}
|
||||
out.Users, out.Chats = r.tgChatlistPeerEnvelope(ctx, userID, allPeers)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsCheckChatlistInvite(ctx context.Context, slug string) (tg.ChatlistsChatlistInviteClass, error) {
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
preview, err := r.deps.Chatlists.CheckInvite(ctx, userID, slug)
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
if preview.LocalFolder != nil {
|
||||
filterID := preview.LocalFolder.ID
|
||||
if preview.Membership != nil {
|
||||
filterID = preview.Membership.LocalFilterID
|
||||
}
|
||||
users, chats, channels := r.tgChatlistPeerEnvelopeWithChannels(ctx, userID, append(preview.Missing, preview.Already...))
|
||||
if preview.Membership != nil {
|
||||
r.pushChatlistJoinedChannelNudges(ctx, userID, channels)
|
||||
}
|
||||
return &tg.ChatlistsChatlistInviteAlready{
|
||||
FilterID: filterID,
|
||||
MissingPeers: tgChatlistPeers(preview.Missing),
|
||||
AlreadyPeers: tgChatlistPeers(preview.Already),
|
||||
Chats: chats,
|
||||
Users: users,
|
||||
}, nil
|
||||
}
|
||||
users, chats := r.tgChatlistPeerEnvelope(ctx, userID, preview.Missing)
|
||||
out := &tg.ChatlistsChatlistInvite{
|
||||
TitleNoanimate: preview.OwnerFolder.TitleNoanimate,
|
||||
Title: tg.TextWithEntities{
|
||||
Text: preview.OwnerFolder.Title,
|
||||
Entities: tgMessageEntities(preview.OwnerFolder.TitleEntities),
|
||||
},
|
||||
Peers: tgChatlistPeers(preview.Missing),
|
||||
Chats: chats,
|
||||
Users: users,
|
||||
}
|
||||
if preview.OwnerFolder.HasEmoticon {
|
||||
out.SetEmoticon(preview.OwnerFolder.Emoticon)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsJoinChatlistInvite(ctx context.Context, req *tg.ChatlistsJoinChatlistInviteRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := r.deps.Chatlists.JoinInvite(ctx, userID, req.Slug, peers, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
return r.chatlistOperationUpdates(ctx, userID, result.Folder.ID, &result.Folder, result.ChannelResults, false)
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsGetChatlistUpdates(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (*tg.ChatlistsChatlistUpdates, error) {
|
||||
if r.deps.Chatlists == nil {
|
||||
return &tg.ChatlistsChatlistUpdates{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
updates, err := r.deps.Chatlists.GetUpdates(ctx, userID, chatlistFilterID(chatlist))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
users, chats := r.tgChatlistPeerEnvelope(ctx, userID, updates.Missing)
|
||||
return &tg.ChatlistsChatlistUpdates{
|
||||
MissingPeers: tgChatlistPeers(updates.Missing),
|
||||
Chats: chats,
|
||||
Users: users,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsJoinChatlistUpdates(ctx context.Context, req *tg.ChatlistsJoinChatlistUpdatesRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := r.deps.Chatlists.JoinUpdates(ctx, userID, chatlistFilterID(req.Chatlist), peers, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
return r.chatlistOperationUpdates(ctx, userID, result.Folder.ID, &result.Folder, result.ChannelResults, false)
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsHideChatlistUpdates(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (bool, error) {
|
||||
if r.deps.Chatlists == nil {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if err := r.deps.Chatlists.HideUpdates(ctx, userID, chatlistFilterID(chatlist)); err != nil {
|
||||
return false, chatlistErr(err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsGetLeaveChatlistSuggestions(ctx context.Context, chatlist tg.InputChatlistDialogFilter) ([]tg.PeerClass, error) {
|
||||
if r.deps.Chatlists == nil {
|
||||
return []tg.PeerClass{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peers, err := r.deps.Chatlists.LeaveSuggestions(ctx, userID, chatlistFilterID(chatlist))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
return tgChatlistPeers(peers), nil
|
||||
}
|
||||
|
||||
func (r *Router) onChatlistsLeaveChatlist(ctx context.Context, req *tg.ChatlistsLeaveChatlistRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Chatlists == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := r.deps.Chatlists.Leave(ctx, userID, chatlistFilterID(req.Chatlist), peers, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, chatlistErr(err)
|
||||
}
|
||||
return r.chatlistOperationUpdates(ctx, userID, result.FilterID, nil, result.ChannelResults, true)
|
||||
}
|
||||
|
||||
func (r *Router) tgExportedChatlistInvite(invite domain.ChatlistInvite) *tg.ExportedChatlistInvite {
|
||||
out := &tg.ExportedChatlistInvite{
|
||||
Title: invite.Title,
|
||||
URL: r.publicLink("addlist/" + invite.Slug),
|
||||
Peers: tgChatlistPeers(invite.Peers),
|
||||
}
|
||||
if invite.Revoked {
|
||||
out.Flags.Set(0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) tgChatlistPeerEnvelope(ctx context.Context, userID int64, peers []domain.DialogFolderPeer) ([]tg.UserClass, []tg.ChatClass) {
|
||||
users, chats, _ := r.tgChatlistPeerEnvelopeWithChannels(ctx, userID, peers)
|
||||
return users, chats
|
||||
}
|
||||
|
||||
func (r *Router) tgChatlistPeerEnvelopeWithChannels(ctx context.Context, userID int64, peers []domain.DialogFolderPeer) ([]tg.UserClass, []tg.ChatClass, []domain.Channel) {
|
||||
userIDs := make([]int64, 0)
|
||||
channelIDs := make([]int64, 0)
|
||||
seenUsers := make(map[int64]struct{})
|
||||
seenChannels := make(map[int64]struct{})
|
||||
for _, item := range peers {
|
||||
switch item.Peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
if item.Peer.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenUsers[item.Peer.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seenUsers[item.Peer.ID] = struct{}{}
|
||||
userIDs = append(userIDs, item.Peer.ID)
|
||||
case domain.PeerTypeChannel:
|
||||
if item.Peer.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenChannels[item.Peer.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seenChannels[item.Peer.ID] = struct{}{}
|
||||
channelIDs = append(channelIDs, item.Peer.ID)
|
||||
}
|
||||
}
|
||||
chats := make([]tg.ChatClass, 0, len(channelIDs))
|
||||
channels := make([]domain.Channel, 0, len(channelIDs))
|
||||
if r.deps.Channels != nil && len(channelIDs) > 0 {
|
||||
if views, err := r.deps.Channels.GetChannels(ctx, userID, channelIDs); err == nil {
|
||||
for _, view := range views {
|
||||
if view.Channel.ID == 0 {
|
||||
continue
|
||||
}
|
||||
channels = append(channels, view.Channel)
|
||||
chats = append(chats, tgChannelChatForView(userID, view))
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.tgUsersForIDs(ctx, userID, userIDs), chats, channels
|
||||
}
|
||||
|
||||
func tgChatlistPeers(peers []domain.DialogFolderPeer) []tg.PeerClass {
|
||||
out := make([]tg.PeerClass, 0, len(peers))
|
||||
seen := make(map[domain.Peer]struct{}, len(peers))
|
||||
for _, item := range peers {
|
||||
if _, ok := seen[item.Peer]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item.Peer] = struct{}{}
|
||||
if peer := tgPeer(item.Peer); peer != nil {
|
||||
out = append(out, peer)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func chatlistFilterID(chatlist tg.InputChatlistDialogFilter) int {
|
||||
return chatlist.FilterID
|
||||
}
|
||||
|
||||
func (r *Router) recordChatlistFilterUpdate(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder) error {
|
||||
event := domain.UpdateEvent{
|
||||
Type: domain.UpdateEventDialogFilter,
|
||||
FilterID: filterID,
|
||||
DialogFilter: folder,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
}
|
||||
var err error
|
||||
if r.deps.Updates != nil {
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
event, _, err = r.deps.Updates.RecordDialogFilter(ctx, authKeyID, userID, filterID, folder, sessionID)
|
||||
if err != nil {
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
r.bookkeepAuxPtsForCurrentSession(ctx, event)
|
||||
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) chatlistFilterUpdates(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder) (tg.UpdatesClass, error) {
|
||||
event := domain.UpdateEvent{
|
||||
Type: domain.UpdateEventDialogFilter,
|
||||
FilterID: filterID,
|
||||
DialogFilter: folder,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
}
|
||||
var err error
|
||||
if r.deps.Updates != nil {
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
event, _, err = r.deps.Updates.RecordDialogFilter(ctx, authKeyID, userID, filterID, folder, sessionID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
}
|
||||
r.bookkeepAuxPtsForCurrentSession(ctx, event)
|
||||
out := tgUpdateForOutboxEvent(event)
|
||||
if out == nil {
|
||||
out = &tg.Updates{Date: event.Date}
|
||||
}
|
||||
if folder != nil {
|
||||
out.Users, out.Chats = r.tgChatlistPeerEnvelope(ctx, userID, append(folder.PinnedPeers, folder.IncludePeers...))
|
||||
}
|
||||
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) chatlistOperationUpdates(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder, channelResults []domain.CreateChannelResult, leaving bool) (tg.UpdatesClass, error) {
|
||||
base, err := r.chatlistFilterUpdates(ctx, userID, filterID, folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, ok := base.(*tg.Updates)
|
||||
if !ok {
|
||||
out = &tg.Updates{Date: int(r.clock.Now().Unix())}
|
||||
}
|
||||
for _, res := range channelResults {
|
||||
if res.Channel.ID == 0 {
|
||||
continue
|
||||
}
|
||||
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
|
||||
if leaving {
|
||||
r.removeOnlineChannelMemberships(res.Channel.ID, userID)
|
||||
r.recordChannelStateForUser(ctx, userID, res.Channel.ID, true)
|
||||
} else {
|
||||
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
||||
}
|
||||
channelUpdates := r.channelOperationUpdates(ctx, userID, res)
|
||||
out = mergeUpdates(out, channelUpdates)
|
||||
if !leaving {
|
||||
if nudge := chatlistJoinedChannelNudge(res.Channel); nudge != nil {
|
||||
out.Updates = append(out.Updates, nudge)
|
||||
}
|
||||
}
|
||||
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
||||
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func chatlistJoinedChannelNudge(channel domain.Channel) tg.UpdateClass {
|
||||
if channel.ID == 0 || channel.Pts <= 0 {
|
||||
return nil
|
||||
}
|
||||
update := &tg.UpdateChannelTooLong{ChannelID: channel.ID}
|
||||
update.SetPts(channel.Pts)
|
||||
return update
|
||||
}
|
||||
|
||||
func (r *Router) pushChatlistJoinedChannelNudges(ctx context.Context, userID int64, channels []domain.Channel) {
|
||||
if userID == 0 || len(channels) == 0 {
|
||||
return
|
||||
}
|
||||
updates := make([]tg.UpdateClass, 0, len(channels))
|
||||
for _, channel := range channels {
|
||||
if nudge := chatlistJoinedChannelNudge(channel); nudge != nil {
|
||||
updates = append(updates, nudge)
|
||||
}
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
out := &tg.Updates{
|
||||
Updates: updates,
|
||||
Users: []tg.UserClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
Seq: 0,
|
||||
}
|
||||
if _, ok := SessionIDFrom(ctx); ok {
|
||||
r.pushCurrentSessionMessage(ctx, "push chatlist joined channel nudges", out)
|
||||
return
|
||||
}
|
||||
r.pushUserUpdates(ctx, userID, out)
|
||||
}
|
||||
|
||||
func chatlistErr(err error) error {
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, domain.ErrInviteRequestSent),
|
||||
errors.Is(err, domain.ErrUserAlreadyParticipant),
|
||||
errors.Is(err, domain.ErrChannelUserBanned),
|
||||
errors.Is(err, domain.ErrUserKicked),
|
||||
errors.Is(err, domain.ErrBotGroupsBlocked):
|
||||
return channelInviteErr(err)
|
||||
case errors.Is(err, domain.ErrChannelUserCreator),
|
||||
errors.Is(err, domain.ErrUserNotParticipant),
|
||||
errors.Is(err, domain.ErrChannelAdminRequired):
|
||||
return channelAdminErr(err)
|
||||
case errors.Is(err, domain.ErrChannelInvalid),
|
||||
errors.Is(err, domain.ErrChannelPrivate):
|
||||
return channelInvalidErr(err)
|
||||
case errors.Is(err, domain.ErrChatlistInviteInvalid):
|
||||
return inviteSlugEmptyErr()
|
||||
case errors.Is(err, domain.ErrChatlistInviteExpired):
|
||||
return inviteSlugExpiredErr()
|
||||
case errors.Is(err, domain.ErrChatlistInvitesTooMuch):
|
||||
return invitesTooMuchErr()
|
||||
case errors.Is(err, domain.ErrChatlistsTooMuch):
|
||||
return chatlistsTooMuchErr()
|
||||
case errors.Is(err, domain.ErrChatlistPeersEmpty):
|
||||
return peersListEmptyErr()
|
||||
case errors.Is(err, domain.ErrChatlistPeersTooMuch):
|
||||
return limitInvalidErr()
|
||||
case errors.Is(err, domain.ErrChatlistNotShareable):
|
||||
return filterNotSupportedErr()
|
||||
case errors.Is(err, domain.ErrChatlistInvalid):
|
||||
return filterIDInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
307
internal/rpc/chatlists_rpc_test.go
Normal file
307
internal/rpc/chatlists_rpc_test.go
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestChatlistsExportInviteRPCShape(t *testing.T) {
|
||||
fake := &fakeChatlistsService{
|
||||
exportFolder: domain.DialogFolder{
|
||||
ID: 2,
|
||||
Title: "Team",
|
||||
IsChatlist: true,
|
||||
HasMyInvites: true,
|
||||
IncludePeers: []domain.DialogFolderPeer{{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42}, AccessHash: 4200}},
|
||||
},
|
||||
exportInvite: domain.ChatlistInvite{
|
||||
OwnerUserID: 1001,
|
||||
FilterID: 2,
|
||||
Slug: "slug-rpc",
|
||||
Title: "Main link",
|
||||
Peers: []domain.DialogFolderPeer{{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42}, AccessHash: 4200}},
|
||||
Date: 100,
|
||||
},
|
||||
}
|
||||
r := New(Config{PublicBaseURL: "http://127.0.0.1:2401"}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
got, err := r.onChatlistsExportChatlistInvite(WithUserID(context.Background(), 1001), &tg.ChatlistsExportChatlistInviteRequest{
|
||||
Chatlist: tg.InputChatlistDialogFilter{FilterID: 2},
|
||||
Title: "Main link",
|
||||
Peers: []tg.InputPeerClass{&tg.InputPeerChannel{ChannelID: 42, AccessHash: 4200}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("export rpc: %v", err)
|
||||
}
|
||||
filter, ok := got.Filter.(*tg.DialogFilterChatlist)
|
||||
if !ok || !filter.HasMyInvites || filter.ID != 2 {
|
||||
t.Fatalf("filter = %#v, want dialogFilterChatlist with has_my_invites", got.Filter)
|
||||
}
|
||||
if got.Invite.URL != "http://127.0.0.1:2401/addlist/slug-rpc" || len(got.Invite.Peers) != 1 {
|
||||
t.Fatalf("invite = %+v, want url and one peer", got.Invite)
|
||||
}
|
||||
if fake.exportPeers[0].Peer.Type != domain.PeerTypeChannel || fake.exportPeers[0].Peer.ID != 42 || fake.exportPeers[0].AccessHash != 4200 {
|
||||
t.Fatalf("service peers = %+v, want parsed input peer", fake.exportPeers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatlistsExportInvitePublicBaseURLIsRouterScoped(t *testing.T) {
|
||||
fake := &fakeChatlistsService{
|
||||
exportFolder: domain.DialogFolder{ID: 2, Title: "Team", IsChatlist: true, HasMyInvites: true},
|
||||
exportInvite: domain.ChatlistInvite{
|
||||
OwnerUserID: 1001,
|
||||
FilterID: 2,
|
||||
Slug: "slug-rpc",
|
||||
Title: "Main link",
|
||||
},
|
||||
}
|
||||
local := New(Config{PublicBaseURL: "http://127.0.0.1:2401"}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
_ = New(Config{PublicBaseURL: "https://telesrv.net"}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
got, err := local.onChatlistsExportChatlistInvite(WithUserID(context.Background(), 1001), &tg.ChatlistsExportChatlistInviteRequest{
|
||||
Chatlist: tg.InputChatlistDialogFilter{FilterID: 2},
|
||||
Title: "Main link",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("export rpc: %v", err)
|
||||
}
|
||||
if got.Invite.URL != "http://127.0.0.1:2401/addlist/slug-rpc" {
|
||||
t.Fatalf("invite url = %q, want local router base URL", got.Invite.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatlistsJoinInviteReturnsDialogFilterUpdate(t *testing.T) {
|
||||
fake := &fakeChatlistsService{
|
||||
joinResult: domain.ChatlistJoinResult{
|
||||
Folder: domain.DialogFolder{
|
||||
ID: 3,
|
||||
Title: "Team",
|
||||
IsChatlist: true,
|
||||
IncludePeers: []domain.DialogFolderPeer{
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42}, AccessHash: 4200},
|
||||
},
|
||||
},
|
||||
ChannelResults: []domain.CreateChannelResult{
|
||||
{
|
||||
Channel: domain.Channel{
|
||||
ID: 42,
|
||||
AccessHash: 4200,
|
||||
Title: "Team channel",
|
||||
Megagroup: true,
|
||||
Pts: 9,
|
||||
},
|
||||
Members: []domain.ChannelMember{
|
||||
{ChannelID: 42, UserID: 2002, Status: domain.ChannelMemberActive},
|
||||
},
|
||||
},
|
||||
},
|
||||
Date: 100,
|
||||
},
|
||||
}
|
||||
r := New(Config{}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
got, err := r.onChatlistsJoinChatlistInvite(WithUserID(context.Background(), 2002), &tg.ChatlistsJoinChatlistInviteRequest{
|
||||
Slug: "slug-rpc",
|
||||
Peers: []tg.InputPeerClass{&tg.InputPeerChannel{ChannelID: 42, AccessHash: 4200}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("join rpc: %v", err)
|
||||
}
|
||||
updates, ok := got.(*tg.Updates)
|
||||
if !ok || len(updates.Updates) == 0 {
|
||||
t.Fatalf("updates = %#v, want updates with updateDialogFilter", got)
|
||||
}
|
||||
update, ok := updates.Updates[0].(*tg.UpdateDialogFilter)
|
||||
if !ok || update.ID != 3 {
|
||||
t.Fatalf("first update = %#v, want updateDialogFilter id=3", updates.Updates[0])
|
||||
}
|
||||
filter, ok := update.Filter.(*tg.DialogFilterChatlist)
|
||||
if !ok || filter.ID != 3 || len(filter.IncludePeers) != 1 {
|
||||
t.Fatalf("update filter = %#v, want joined chatlist filter", update.Filter)
|
||||
}
|
||||
if fake.joinPeers[0].Peer.Type != domain.PeerTypeChannel || fake.joinPeers[0].Peer.ID != 42 || fake.joinPeers[0].AccessHash != 4200 {
|
||||
t.Fatalf("service join peers = %+v, want parsed input peer", fake.joinPeers)
|
||||
}
|
||||
var gotChannelNudge bool
|
||||
for _, u := range updates.Updates {
|
||||
nudge, ok := u.(*tg.UpdateChannelTooLong)
|
||||
if !ok || nudge.ChannelID != 42 {
|
||||
continue
|
||||
}
|
||||
pts, ok := nudge.GetPts()
|
||||
if !ok || pts != 9 {
|
||||
t.Fatalf("channel nudge pts = %d ok %v, want 9 true", pts, ok)
|
||||
}
|
||||
gotChannelNudge = true
|
||||
}
|
||||
if !gotChannelNudge {
|
||||
t.Fatalf("updates = %#v, want UpdateChannelTooLong for joined channel", updates.Updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatlistsJoinedChannelNudgesCarryPts(t *testing.T) {
|
||||
sessions := &captureSessions{}
|
||||
r := New(Config{}, Deps{Sessions: sessions}, zap.NewNop(), clock.System)
|
||||
r.pushChatlistJoinedChannelNudges(context.Background(), 2002, []domain.Channel{
|
||||
{ID: 42, Pts: 9},
|
||||
{ID: 43},
|
||||
})
|
||||
updates, ok := sessions.lastUserPush().(*tg.Updates)
|
||||
if !ok || len(updates.Updates) != 1 {
|
||||
t.Fatalf("pushed updates = %#v, want one UpdateChannelTooLong", sessions.lastUserPush())
|
||||
}
|
||||
nudge, ok := updates.Updates[0].(*tg.UpdateChannelTooLong)
|
||||
if !ok || nudge.ChannelID != 42 {
|
||||
t.Fatalf("pushed update = %#v, want channel 42 nudge", updates.Updates[0])
|
||||
}
|
||||
pts, ok := nudge.GetPts()
|
||||
if !ok || pts != 9 {
|
||||
t.Fatalf("pushed nudge pts = %d ok %v, want 9 true", pts, ok)
|
||||
}
|
||||
|
||||
sessions.clearMessages()
|
||||
r.pushChatlistJoinedChannelNudges(WithSessionID(context.Background(), 77), 2002, []domain.Channel{{ID: 44, Pts: 11}})
|
||||
snap := sessions.snapshot()
|
||||
currentUpdates, ok := snap.message.(*tg.Updates)
|
||||
if !ok || len(currentUpdates.Updates) != 1 {
|
||||
t.Fatalf("current session push = %#v, want one UpdateChannelTooLong", snap.message)
|
||||
}
|
||||
if ids := sessions.pushedUserIDs(); len(ids) != 0 {
|
||||
t.Fatalf("current session nudge pushed to user ids = %v, want none", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatlistsCheckInviteRPCAlreadyAndFreshShapes(t *testing.T) {
|
||||
peer := domain.DialogFolderPeer{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42}, AccessHash: 4200}
|
||||
fake := &fakeChatlistsService{
|
||||
checkPreview: domain.ChatlistInvitePreview{
|
||||
Invite: domain.ChatlistInvite{Slug: "slug-rpc", Peers: []domain.DialogFolderPeer{peer}},
|
||||
OwnerFolder: domain.DialogFolder{
|
||||
ID: 2,
|
||||
Title: "Team",
|
||||
HasEmoticon: true,
|
||||
Emoticon: ":)",
|
||||
IncludePeers: []domain.DialogFolderPeer{peer},
|
||||
},
|
||||
Missing: []domain.DialogFolderPeer{peer},
|
||||
},
|
||||
}
|
||||
r := New(Config{}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
fresh, err := r.onChatlistsCheckChatlistInvite(WithUserID(context.Background(), 2002), "slug-rpc")
|
||||
if err != nil {
|
||||
t.Fatalf("check fresh rpc: %v", err)
|
||||
}
|
||||
invite, ok := fresh.(*tg.ChatlistsChatlistInvite)
|
||||
if !ok || invite.Title.Text != "Team" || len(invite.Peers) != 1 {
|
||||
t.Fatalf("fresh = %#v, want chatlistInvite with title and peer", fresh)
|
||||
}
|
||||
if emoticon, ok := invite.GetEmoticon(); !ok || emoticon != ":)" {
|
||||
t.Fatalf("fresh emoticon = %q ok %v, want folder emoji", emoticon, ok)
|
||||
}
|
||||
|
||||
local := domain.DialogFolder{ID: 5, Title: "Team", IsChatlist: true, IncludePeers: []domain.DialogFolderPeer{peer}}
|
||||
fake.checkPreview.LocalFolder = &local
|
||||
fake.checkPreview.Membership = &domain.ChatlistMembership{UserID: 2002, LocalFilterID: 5, Slug: "slug-rpc"}
|
||||
fake.checkPreview.Missing = nil
|
||||
fake.checkPreview.Already = []domain.DialogFolderPeer{peer}
|
||||
already, err := r.onChatlistsCheckChatlistInvite(WithUserID(context.Background(), 2002), "slug-rpc")
|
||||
if err != nil {
|
||||
t.Fatalf("check already rpc: %v", err)
|
||||
}
|
||||
gotAlready, ok := already.(*tg.ChatlistsChatlistInviteAlready)
|
||||
if !ok || gotAlready.FilterID != 5 || len(gotAlready.AlreadyPeers) != 1 {
|
||||
t.Fatalf("already = %#v, want already filter id and peer", already)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatlistsEditInviteRevokedFlagRoundTrips(t *testing.T) {
|
||||
fake := &fakeChatlistsService{
|
||||
exportInvite: domain.ChatlistInvite{
|
||||
OwnerUserID: 1001,
|
||||
FilterID: 2,
|
||||
Slug: "slug-rpc",
|
||||
Title: "Main link",
|
||||
Revoked: true,
|
||||
},
|
||||
}
|
||||
r := New(Config{PublicBaseURL: "https://telesrv.net"}, Deps{Chatlists: fake}, zap.NewNop(), clock.System)
|
||||
var flags bin.Fields
|
||||
flags.Set(0)
|
||||
got, err := r.onChatlistsEditExportedInvite(WithUserID(context.Background(), 1001), &tg.ChatlistsEditExportedInviteRequest{
|
||||
Flags: flags,
|
||||
Chatlist: tg.InputChatlistDialogFilter{FilterID: 2},
|
||||
Slug: "slug-rpc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("edit rpc: %v", err)
|
||||
}
|
||||
if !fake.editRevoke {
|
||||
t.Fatalf("service revoke = false, want true")
|
||||
}
|
||||
if !got.Flags.Has(0) {
|
||||
t.Fatalf("edited invite flags = %v, want revoked bit", got.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeChatlistsService struct {
|
||||
exportFolder domain.DialogFolder
|
||||
exportInvite domain.ChatlistInvite
|
||||
exportPeers []domain.DialogFolderPeer
|
||||
checkPreview domain.ChatlistInvitePreview
|
||||
joinResult domain.ChatlistJoinResult
|
||||
joinPeers []domain.DialogFolderPeer
|
||||
updates domain.ChatlistUpdates
|
||||
joinUpdFolder domain.DialogFolder
|
||||
editRevoke bool
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) ExportInvite(_ context.Context, _ int64, _ int, _ string, peers []domain.DialogFolderPeer, _ int) (domain.DialogFolder, domain.ChatlistInvite, error) {
|
||||
f.exportPeers = append([]domain.DialogFolderPeer(nil), peers...)
|
||||
return f.exportFolder, f.exportInvite, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) ListInvites(context.Context, int64, int) ([]domain.ChatlistInvite, error) {
|
||||
return []domain.ChatlistInvite{f.exportInvite}, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) EditInvite(_ context.Context, _ int64, _ int, _ string, _ *string, _ *[]domain.DialogFolderPeer, revoke bool) (domain.ChatlistInvite, error) {
|
||||
f.editRevoke = revoke
|
||||
return f.exportInvite, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) DeleteInvite(context.Context, int64, int, string) (domain.DialogFolder, bool, error) {
|
||||
return domain.DialogFolder{}, false, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) CheckInvite(context.Context, int64, string) (domain.ChatlistInvitePreview, error) {
|
||||
return f.checkPreview, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) JoinInvite(_ context.Context, _ int64, _ string, peers []domain.DialogFolderPeer, _ int) (domain.ChatlistJoinResult, error) {
|
||||
f.joinPeers = append([]domain.DialogFolderPeer(nil), peers...)
|
||||
return f.joinResult, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) GetUpdates(context.Context, int64, int) (domain.ChatlistUpdates, error) {
|
||||
return f.updates, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) JoinUpdates(context.Context, int64, int, []domain.DialogFolderPeer, int) (domain.ChatlistJoinResult, error) {
|
||||
return domain.ChatlistJoinResult{Folder: f.joinUpdFolder}, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) HideUpdates(context.Context, int64, int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) Leave(context.Context, int64, int, []domain.DialogFolderPeer, int) (domain.ChatlistLeaveResult, error) {
|
||||
return domain.ChatlistLeaveResult{FilterID: 3}, nil
|
||||
}
|
||||
|
||||
func (f *fakeChatlistsService) LeaveSuggestions(context.Context, int64, int) ([]domain.DialogFolderPeer, error) {
|
||||
return f.exportInvite.Peers, nil
|
||||
}
|
||||
|
|
@ -466,7 +466,7 @@ func tgChannel(viewerUserID int64, ch domain.Channel, self *domain.ChannelMember
|
|||
return out
|
||||
}
|
||||
|
||||
func tgChannelFull(view domain.ChannelView) *tg.ChannelFull {
|
||||
func tgChannelFull(view domain.ChannelView, publicBaseURL ...string) *tg.ChannelFull {
|
||||
ch := view.Channel
|
||||
full := &tg.ChannelFull{
|
||||
// 广播频道的订阅者列表仅管理员可见(官方语义):非管理员订阅者拿到
|
||||
|
|
@ -496,7 +496,11 @@ func tgChannelFull(view domain.ChannelView) *tg.ChannelFull {
|
|||
full.SetAvailableMinID(view.Self.AvailableMinID)
|
||||
}
|
||||
if view.ExportedInvite != nil && !view.ExportedInvite.Revoked {
|
||||
full.SetExportedInvite(tgExportedChannelInvite(*view.ExportedInvite))
|
||||
baseURL := ""
|
||||
if len(publicBaseURL) > 0 {
|
||||
baseURL = publicBaseURL[0]
|
||||
}
|
||||
full.SetExportedInvite(tgExportedChannelInvite(*view.ExportedInvite, baseURL))
|
||||
}
|
||||
if ch.AdminsCount > 0 {
|
||||
full.SetAdminsCount(ch.AdminsCount)
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ func tgDialogFilter(folder domain.DialogFolder) tg.DialogFilterClass {
|
|||
title := tg.TextWithEntities{Text: folder.Title, Entities: tgMessageEntities(folder.TitleEntities)}
|
||||
if folder.IsChatlist {
|
||||
out := &tg.DialogFilterChatlist{
|
||||
HasMyInvites: folder.HasMyInvites,
|
||||
TitleNoanimate: folder.TitleNoanimate,
|
||||
ID: folder.ID,
|
||||
Title: title,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const groupCallUnmutedVideoLimit = 30
|
|||
var groupCallStreamDCID = 2
|
||||
|
||||
// tgGroupCall 把 call 行转为 TL groupCall。
|
||||
func tgGroupCall(call domain.GroupCall, viewerUserID int64, canManage bool) tg.GroupCallClass {
|
||||
func tgGroupCall(call domain.GroupCall, viewerUserID int64, canManage bool, publicBaseURL ...string) tg.GroupCallClass {
|
||||
if !call.Active() {
|
||||
return &tg.GroupCallDiscarded{ID: call.ID, AccessHash: call.AccessHash, Duration: call.Duration}
|
||||
}
|
||||
|
|
@ -64,7 +64,11 @@ func tgGroupCall(call domain.GroupCall, viewerUserID int64, canManage bool) tg.G
|
|||
out.SetTitle(call.Title)
|
||||
}
|
||||
if call.Conference() {
|
||||
if link := conferenceCanonicalInviteLink(call.InviteSlug); link != "" {
|
||||
baseURL := ""
|
||||
if len(publicBaseURL) > 0 {
|
||||
baseURL = publicBaseURL[0]
|
||||
}
|
||||
if link := conferenceCanonicalInviteLink(call.InviteSlug, baseURL); link != "" {
|
||||
out.SetInviteLink(link)
|
||||
} else if call.InviteLink != "" {
|
||||
out.SetInviteLink(call.InviteLink)
|
||||
|
|
|
|||
|
|
@ -400,6 +400,21 @@ type DialogsService interface {
|
|||
EditPeerFolders(ctx context.Context, userID int64, peers []domain.FolderPeerUpdate) error
|
||||
}
|
||||
|
||||
// ChatlistsService 抽象 Telegram shared folders / chatlists 业务。
|
||||
type ChatlistsService interface {
|
||||
ExportInvite(ctx context.Context, userID int64, filterID int, title string, peers []domain.DialogFolderPeer, date int) (domain.DialogFolder, domain.ChatlistInvite, error)
|
||||
ListInvites(ctx context.Context, userID int64, filterID int) ([]domain.ChatlistInvite, error)
|
||||
EditInvite(ctx context.Context, userID int64, filterID int, slug string, title *string, peers *[]domain.DialogFolderPeer, revoke bool) (domain.ChatlistInvite, error)
|
||||
DeleteInvite(ctx context.Context, userID int64, filterID int, slug string) (domain.DialogFolder, bool, error)
|
||||
CheckInvite(ctx context.Context, userID int64, slug string) (domain.ChatlistInvitePreview, error)
|
||||
JoinInvite(ctx context.Context, userID int64, slug string, peers []domain.DialogFolderPeer, date int) (domain.ChatlistJoinResult, error)
|
||||
GetUpdates(ctx context.Context, userID int64, localFilterID int) (domain.ChatlistUpdates, error)
|
||||
JoinUpdates(ctx context.Context, userID int64, localFilterID int, peers []domain.DialogFolderPeer, date int) (domain.ChatlistJoinResult, error)
|
||||
HideUpdates(ctx context.Context, userID int64, localFilterID int) error
|
||||
Leave(ctx context.Context, userID int64, localFilterID int, peers []domain.DialogFolderPeer, date int) (domain.ChatlistLeaveResult, error)
|
||||
LeaveSuggestions(ctx context.Context, userID int64, localFilterID int) ([]domain.DialogFolderPeer, error)
|
||||
}
|
||||
|
||||
// MessagesService 抽象消息历史、搜索与已读。
|
||||
type MessagesService interface {
|
||||
SendPrivateText(ctx context.Context, userID int64, req domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error)
|
||||
|
|
@ -697,6 +712,7 @@ type Deps struct {
|
|||
BootstrapUpdates store.BootstrapUpdateJobStore
|
||||
Contacts ContactsService
|
||||
Dialogs DialogsService
|
||||
Chatlists ChatlistsService
|
||||
Messages MessagesService
|
||||
Stories StoriesService
|
||||
Channels ChannelsService
|
||||
|
|
|
|||
|
|
@ -41,6 +41,18 @@ func filterIncludeEmptyErr() error { return tgerr.New(400, "FILTER_INCLUDE_EMPTY
|
|||
|
||||
func filterTitleEmptyErr() error { return tgerr.New(400, "FILTER_TITLE_EMPTY") }
|
||||
|
||||
func filterNotSupportedErr() error { return tgerr.New(400, "FILTER_NOT_SUPPORTED") }
|
||||
|
||||
func inviteSlugEmptyErr() error { return tgerr.New(400, "INVITE_SLUG_EMPTY") }
|
||||
|
||||
func inviteSlugExpiredErr() error { return tgerr.New(400, "INVITE_SLUG_EXPIRED") }
|
||||
|
||||
func invitesTooMuchErr() error { return tgerr.New(400, "INVITES_TOO_MUCH") }
|
||||
|
||||
func chatlistsTooMuchErr() error { return tgerr.New(400, "CHATLISTS_TOO_MUCH") }
|
||||
|
||||
func peersListEmptyErr() error { return tgerr.New(400, "PEERS_LIST_EMPTY") }
|
||||
|
||||
// peerIDInvalidErr 表示目标 peer 不存在或当前阶段不支持。
|
||||
func peerIDInvalidErr() error { return tgerr.New(400, "PEER_ID_INVALID") }
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
// registerHelp 注册 help.* RPC handler(DC 配置、最近 DC)。
|
||||
func (r *Router) registerHelp(d *tg.ServerDispatcher) {
|
||||
d.OnHelpGetConfig(func(ctx context.Context) (*tg.Config, error) {
|
||||
return tdesktop.BuildConfig(r.cfg.DC, r.cfg.IP, r.cfg.Port, r.clock.Now()), nil
|
||||
return tdesktop.BuildConfig(r.cfg.DC, r.cfg.IP, r.cfg.Port, r.clock.Now(), r.cfg.PublicBaseURL), nil
|
||||
})
|
||||
d.OnHelpGetNearestDC(func(ctx context.Context) (*tg.NearestDC, error) {
|
||||
return tdesktop.NearestDC(r.cfg.DC), nil
|
||||
|
|
|
|||
|
|
@ -966,6 +966,7 @@ func (r *Router) dialogFolderFromTG(ctx context.Context, userID int64, id int, f
|
|||
color, hasColor := f.GetColor()
|
||||
return domain.DialogFolder{
|
||||
ID: id,
|
||||
HasMyInvites: f.HasMyInvites,
|
||||
TitleNoanimate: f.TitleNoanimate,
|
||||
Title: title,
|
||||
TitleEntities: domainMessageEntities(f.Title.Entities),
|
||||
|
|
|
|||
|
|
@ -419,6 +419,11 @@ func (r *Router) domainFolderPeerFromInputPeer(ctx context.Context, userID int64
|
|||
return domain.Peer{}, 0, peerIDInvalidErr()
|
||||
}
|
||||
return out, 0, nil
|
||||
case *tg.InputPeerChat:
|
||||
if p.ChatID <= 0 {
|
||||
return domain.Peer{}, 0, peerIDInvalidErr()
|
||||
}
|
||||
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChatID}, 0, nil
|
||||
case *tg.InputPeerSelf:
|
||||
if userID == 0 {
|
||||
return domain.Peer{}, 0, peerIDInvalidErr()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net/url"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
|
@ -40,7 +41,7 @@ func (r *Router) onPhoneCreateConferenceCall(ctx context.Context, req *tg.PhoneC
|
|||
return nil, groupCallErr(err)
|
||||
}
|
||||
out := r.groupCallUpdateContainer(ctx, userID, domain.Channel{},
|
||||
&tg.UpdateGroupCall{Call: tgGroupCall(call, userID, true)}, []int64{userID})
|
||||
&tg.UpdateGroupCall{Call: tgGroupCall(call, userID, true, r.cfg.PublicBaseURL)}, []int64{userID})
|
||||
if !req.Join {
|
||||
return out, nil
|
||||
}
|
||||
|
|
@ -81,7 +82,7 @@ func (r *Router) onPhoneExportGroupCallInvite(ctx context.Context, req *tg.Phone
|
|||
return nil, groupCallInvalidErr()
|
||||
}
|
||||
if scope.call.Conference() {
|
||||
link := conferenceExportInviteLink(scope.call)
|
||||
link := conferenceExportInviteLink(scope.call, r.cfg.PublicBaseURL)
|
||||
if link == "" {
|
||||
return nil, groupCallInvalidErr()
|
||||
}
|
||||
|
|
@ -93,21 +94,25 @@ func (r *Router) onPhoneExportGroupCallInvite(ctx context.Context, req *tg.Phone
|
|||
if scope.channel.Username == "" {
|
||||
return nil, publicChannelMissingErr()
|
||||
}
|
||||
return &tg.PhoneExportedGroupCallInvite{Link: "https://telesrv.net/" + scope.channel.Username}, nil
|
||||
return &tg.PhoneExportedGroupCallInvite{Link: r.publicLink(scope.channel.Username)}, nil
|
||||
}
|
||||
|
||||
func conferenceExportInviteLink(call domain.GroupCall) string {
|
||||
if link := conferenceCanonicalInviteLink(call.InviteSlug); link != "" {
|
||||
func conferenceExportInviteLink(call domain.GroupCall, publicBaseURL ...string) string {
|
||||
if link := conferenceCanonicalInviteLink(call.InviteSlug, publicBaseURL...); link != "" {
|
||||
return link
|
||||
}
|
||||
return call.InviteLink
|
||||
}
|
||||
|
||||
func conferenceCanonicalInviteLink(slug string) string {
|
||||
func conferenceCanonicalInviteLink(slug string, publicBaseURL ...string) string {
|
||||
if slug == "" {
|
||||
return ""
|
||||
}
|
||||
return "https://telesrv.net/call/" + slug + "?slug=" + slug
|
||||
baseURL := ""
|
||||
if len(publicBaseURL) > 0 {
|
||||
baseURL = publicBaseURL[0]
|
||||
}
|
||||
return publicLinkQueryWithBaseURL(baseURL, "call/"+slug, url.Values{"slug": []string{slug}})
|
||||
}
|
||||
|
||||
func (r *Router) onPhoneInviteConferenceCallParticipant(ctx context.Context, req *tg.PhoneInviteConferenceCallParticipantRequest) (tg.UpdatesClass, error) {
|
||||
|
|
@ -208,7 +213,7 @@ func (r *Router) onPhoneDeclineConferenceCallInvite(ctx context.Context, msgID i
|
|||
}
|
||||
r.pushConferenceGroupCallUpdate(ctx, call)
|
||||
return r.groupCallUpdateContainer(ctx, userID, domain.Channel{},
|
||||
&tg.UpdateGroupCall{Call: tgGroupCall(call, userID, userID == call.CreatorUserID)}, []int64{inv.InviterUserID, inv.InviteeUserID}), nil
|
||||
&tg.UpdateGroupCall{Call: tgGroupCall(call, userID, userID == call.CreatorUserID, r.cfg.PublicBaseURL)}, []int64{inv.InviterUserID, inv.InviteeUserID}), nil
|
||||
}
|
||||
|
||||
func (r *Router) onPhoneDeleteConferenceCallParticipants(ctx context.Context, req *tg.PhoneDeleteConferenceCallParticipantsRequest) (tg.UpdatesClass, error) {
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ func (r *Router) onPhoneCreateGroupCall(ctx context.Context, req *tg.PhoneCreate
|
|||
r.pushGroupCallServiceMessage(ctx, userID, serviceRes)
|
||||
}
|
||||
// 响应:updateGroupCall + 服务消息(发起设备视角)。TDesktop 创建后自行 joinGroupCall。
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, userID, true)}
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, userID, true, r.cfg.PublicBaseURL)}
|
||||
update.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
out := r.groupCallUpdateContainer(ctx, userID, channel, update, []int64{userID})
|
||||
if serviceRes.Event.Pts != 0 {
|
||||
|
|
@ -368,7 +368,7 @@ func (r *Router) onPhoneJoinGroupCall(ctx context.Context, req *tg.PhoneJoinGrou
|
|||
Version: mut.Call.Version,
|
||||
}, []int64{scope.userID})
|
||||
out.Updates = append(out.Updates, &tg.UpdateGroupCallConnection{Params: tg.DataJSON{Data: params}})
|
||||
callUpdate := &tg.UpdateGroupCall{Call: tgGroupCall(mut.Call, scope.userID, scope.canManage())}
|
||||
callUpdate := &tg.UpdateGroupCall{Call: tgGroupCall(mut.Call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL)}
|
||||
if channel.ID != 0 {
|
||||
callUpdate.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
}
|
||||
|
|
@ -395,7 +395,7 @@ func (r *Router) onPhoneLeaveGroupCall(ctx context.Context, req *tg.PhoneLeaveGr
|
|||
if errors.Is(err, domain.ErrGroupCallNotJoined) {
|
||||
// 幂等:重复 leave / sweeper 已清,返回当前快照。
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
|
||||
groupCallUpdateFor(scope.channel, scope.call, scope.userID, false), nil), nil
|
||||
groupCallUpdateFor(scope.channel, scope.call, scope.userID, false, r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, groupCallErr(err)
|
||||
|
|
@ -411,7 +411,7 @@ func (r *Router) onPhoneLeaveGroupCall(ctx context.Context, req *tg.PhoneLeaveGr
|
|||
Version: mut.Call.Version,
|
||||
}, []int64{scope.userID})
|
||||
if mut.Call.Conference() && !mut.Call.Active() {
|
||||
out.Updates = append(out.Updates, groupCallUpdateFor(domain.Channel{}, mut.Call, scope.userID, scope.userID == mut.Call.CreatorUserID))
|
||||
out.Updates = append(out.Updates, groupCallUpdateFor(domain.Channel{}, mut.Call, scope.userID, scope.userID == mut.Call.CreatorUserID, r.cfg.PublicBaseURL))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
@ -435,7 +435,7 @@ func (r *Router) onPhoneDiscardGroupCall(ctx context.Context, in tg.InputGroupCa
|
|||
if call.Conference() {
|
||||
r.pushConferenceGroupCallUpdateTo(ctx, call, groupCallParticipantUserIDs(activeBeforeDiscard))
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, domain.Channel{},
|
||||
groupCallUpdateFor(domain.Channel{}, call, scope.userID, true), nil), nil
|
||||
groupCallUpdateFor(domain.Channel{}, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
// RTMP 直播结束:断开推流并清空缓冲(观众后续拉流转 resync/停止)。
|
||||
if call.RtmpStream && r.deps.LiveStreams != nil {
|
||||
|
|
@ -463,7 +463,7 @@ func (r *Router) onPhoneDiscardGroupCall(ctx context.Context, in tg.InputGroupCa
|
|||
r.pushGroupCallServiceMessage(ctx, scope.userID, serviceRes)
|
||||
}
|
||||
out := r.groupCallUpdateContainer(ctx, scope.userID, channel,
|
||||
groupCallUpdateFor(channel, call, scope.userID, true), nil)
|
||||
groupCallUpdateFor(channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil)
|
||||
if serviceRes.Event.Pts != 0 {
|
||||
if msgUpdate := tgChannelUpdate(scope.userID, serviceRes.Event); msgUpdate != nil {
|
||||
out.Updates = append(out.Updates, msgUpdate)
|
||||
|
|
@ -499,7 +499,7 @@ func (r *Router) onPhoneGetGroupCall(ctx context.Context, req *tg.PhoneGetGroupC
|
|||
// 定时通话:回填 viewer 自己的开播提醒订阅(客户端 reload 全量重建本地状态)。
|
||||
call := r.applyScheduleSubscription(ctx, scope.call, scope.userID)
|
||||
return &tg.PhoneGroupCall{
|
||||
Call: tgGroupCall(call, scope.userID, scope.canManage()),
|
||||
Call: tgGroupCall(call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL),
|
||||
Participants: tgGroupCallParticipants(page.Participants, scope.userID),
|
||||
ParticipantsNextOffset: page.NextOffset,
|
||||
Chats: chats,
|
||||
|
|
@ -589,8 +589,8 @@ func (r *Router) onPhoneCheckGroupCall(ctx context.Context, req *tg.PhoneCheckGr
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func groupCallUpdateFor(channel domain.Channel, call domain.GroupCall, viewerUserID int64, canManage bool) *tg.UpdateGroupCall {
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, viewerUserID, canManage)}
|
||||
func groupCallUpdateFor(channel domain.Channel, call domain.GroupCall, viewerUserID int64, canManage bool, publicBaseURL ...string) *tg.UpdateGroupCall {
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, viewerUserID, canManage, publicBaseURL...)}
|
||||
if channel.ID != 0 {
|
||||
update.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ func (r *Router) onPhoneEditGroupCallTitle(ctx context.Context, req *tg.PhoneEdi
|
|||
}
|
||||
r.pushGroupCallUpdate(ctx, scope.channel, call)
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
|
||||
groupCallUpdateFor(scope.channel, call, scope.userID, true), nil), nil
|
||||
groupCallUpdateFor(scope.channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
|
||||
func (r *Router) onPhoneToggleGroupCallSettings(ctx context.Context, req *tg.PhoneToggleGroupCallSettingsRequest) (tg.UpdatesClass, error) {
|
||||
|
|
@ -261,7 +261,7 @@ func (r *Router) onPhoneToggleGroupCallSettings(ctx context.Context, req *tg.Pho
|
|||
}
|
||||
r.pushGroupCallUpdate(ctx, scope.channel, call)
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
|
||||
groupCallUpdateFor(scope.channel, call, scope.userID, true), nil), nil
|
||||
groupCallUpdateFor(scope.channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
|
||||
const maxInviteToGroupCallUsers = 10
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func (r *Router) onPhoneLeaveGroupCallPresentation(ctx context.Context, call tg.
|
|||
// 幂等快照:未在共享/已清理时返回当前态而非报错(屏幕共享既已下架即达成目的)。
|
||||
idempotentSnapshot := func() (tg.UpdatesClass, error) {
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
|
||||
groupCallUpdateFor(scope.channel, scope.call, scope.userID, scope.canManage()), nil), nil
|
||||
groupCallUpdateFor(scope.channel, scope.call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
self, found, err := r.deps.GroupCalls.Participant(ctx, scope.call.ID, scope.userID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func (r *Router) joinRtmpGroupCall(ctx context.Context, scope *groupCallScope, r
|
|||
// updateGroupCall 必须先于 updateGroupCallConnection:TDesktop 按序 applyUpdates,
|
||||
// 处理 connection 时若还没从 groupCall 读到 stream_dc_id 会打
|
||||
// "Api Error: Empty stream_dc_id" 并 fallback 主 DC。
|
||||
callUpdate := &tg.UpdateGroupCall{Call: tgGroupCall(mut.Call, scope.userID, scope.canManage())}
|
||||
callUpdate := &tg.UpdateGroupCall{Call: tgGroupCall(mut.Call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL)}
|
||||
if channel.ID != 0 {
|
||||
callUpdate.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func (r *Router) onPhoneStartScheduledGroupCall(ctx context.Context, in tg.Input
|
|||
if !changed {
|
||||
// 幂等:已开始,只回快照,不重复扇出/服务消息。
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, channel,
|
||||
groupCallUpdateFor(channel, call, scope.userID, true), nil), nil
|
||||
groupCallUpdateFor(channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil), nil
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
// started 服务消息(与即时创建的 started 同构)。
|
||||
|
|
@ -75,7 +75,7 @@ func (r *Router) onPhoneStartScheduledGroupCall(ctx context.Context, in tg.Input
|
|||
r.pushGroupCallServiceMessage(ctx, scope.userID, serviceRes)
|
||||
}
|
||||
out := r.groupCallUpdateContainer(ctx, scope.userID, channel,
|
||||
groupCallUpdateFor(channel, call, scope.userID, true), nil)
|
||||
groupCallUpdateFor(channel, call, scope.userID, true, r.cfg.PublicBaseURL), nil)
|
||||
if serviceRes.Event.Pts != 0 {
|
||||
if msgUpdate := tgChannelUpdate(scope.userID, serviceRes.Event); msgUpdate != nil {
|
||||
out.Updates = append(out.Updates, msgUpdate)
|
||||
|
|
@ -105,7 +105,7 @@ func (r *Router) onPhoneToggleGroupCallStartSubscription(ctx context.Context, re
|
|||
call := scope.call
|
||||
call.ScheduleStartSubscribed = req.Subscribed
|
||||
// 订阅是 per-viewer 私有状态:响应给本设备,推送同步本人其它在线设备即可。
|
||||
update := groupCallUpdateFor(scope.channel, call, scope.userID, scope.canManage())
|
||||
update := groupCallUpdateFor(scope.channel, call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL)
|
||||
r.pushUserMessage(ctx, scope.userID, "schedule subscription update",
|
||||
r.groupCallUpdateContainer(ctx, scope.userID, scope.channel, update, nil))
|
||||
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel, update, nil), nil
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func (r *Router) pushGroupCallUpdate(ctx context.Context, channel domain.Channel
|
|||
if subscribed != nil {
|
||||
_, viewerCall.ScheduleStartSubscribed = subscribed[viewerID]
|
||||
}
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(viewerCall, viewerID, false)}
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(viewerCall, viewerID, false, r.cfg.PublicBaseURL)}
|
||||
update.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
r.pushUserMessage(ctx, viewerID, "group call update",
|
||||
r.groupCallUpdateContainer(ctx, viewerID, channel, update, []int64{call.CreatorUserID}))
|
||||
|
|
@ -138,7 +138,7 @@ func (r *Router) pushConferenceGroupCallUpdate(ctx context.Context, call domain.
|
|||
func (r *Router) pushConferenceGroupCallUpdateTo(ctx context.Context, call domain.GroupCall, extraUserIDs []int64) {
|
||||
recipients := r.conferenceCallRecipientsWith(ctx, call.ID, extraUserIDs)
|
||||
for _, viewerID := range recipients {
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, viewerID, viewerID == call.CreatorUserID)}
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, viewerID, viewerID == call.CreatorUserID, r.cfg.PublicBaseURL)}
|
||||
r.pushUserMessage(ctx, viewerID, "conference call update",
|
||||
r.groupCallUpdateContainer(ctx, viewerID, domain.Channel{}, update, []int64{call.CreatorUserID}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func (r *Router) onPremiumGetBoostsStatus(ctx context.Context, peer tg.InputPeer
|
|||
if err != nil {
|
||||
return nil, premiumBoostErr(err)
|
||||
}
|
||||
return tgPremiumBoostsStatus(view.Channel.ID, status), nil
|
||||
return tgPremiumBoostsStatus(view.Channel.ID, status, r.cfg.PublicBaseURL), nil
|
||||
}
|
||||
|
||||
func (r *Router) onPremiumGetBoostsList(ctx context.Context, req *tg.PremiumGetBoostsListRequest) (*tg.PremiumBoostsList, error) {
|
||||
|
|
@ -190,13 +190,13 @@ func (r *Router) currentPremiumUntil(ctx context.Context) (int64, int, error) {
|
|||
return userID, u.PremiumUntil, nil
|
||||
}
|
||||
|
||||
func tgPremiumBoostsStatus(channelID int64, in domain.PremiumBoostStatus) *tg.PremiumBoostsStatus {
|
||||
func tgPremiumBoostsStatus(channelID int64, in domain.PremiumBoostStatus, publicBaseURL string) *tg.PremiumBoostsStatus {
|
||||
out := &tg.PremiumBoostsStatus{
|
||||
MyBoost: len(in.MyBoostSlots) > 0,
|
||||
Level: in.Level,
|
||||
CurrentLevelBoosts: in.CurrentLevelBoosts,
|
||||
Boosts: in.Boosts,
|
||||
BoostURL: fmt.Sprintf("https://telesrv.net/boost?c=%d", channelID),
|
||||
BoostURL: publicLinkParamWithBaseURL(publicBaseURL, "boost", "c", fmt.Sprintf("%d", channelID)),
|
||||
}
|
||||
if in.GiftBoosts > 0 {
|
||||
out.SetGiftBoosts(in.GiftBoosts)
|
||||
|
|
|
|||
35
internal/rpc/public_links.go
Normal file
35
internal/rpc/public_links.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"telesrv/internal/links"
|
||||
)
|
||||
|
||||
func (r *Router) publicLink(path string) string {
|
||||
return publicLinkWithBaseURL(r.cfg.PublicBaseURL, path)
|
||||
}
|
||||
|
||||
func (r *Router) publicLinkQuery(path string, query url.Values) string {
|
||||
return publicLinkQueryWithBaseURL(r.cfg.PublicBaseURL, path, query)
|
||||
}
|
||||
|
||||
func (r *Router) publicLinkParam(path, key, value string) string {
|
||||
return r.publicLinkQuery(path, url.Values{key: []string{value}})
|
||||
}
|
||||
|
||||
func (r *Router) publicLinkHost() string {
|
||||
return links.Host(r.cfg.PublicBaseURL)
|
||||
}
|
||||
|
||||
func publicLinkWithBaseURL(baseURL, path string) string {
|
||||
return links.Build(baseURL, path, nil)
|
||||
}
|
||||
|
||||
func publicLinkQueryWithBaseURL(baseURL, path string, query url.Values) string {
|
||||
return links.Build(baseURL, path, query)
|
||||
}
|
||||
|
||||
func publicLinkParamWithBaseURL(baseURL, path, key, value string) string {
|
||||
return publicLinkQueryWithBaseURL(baseURL, path, url.Values{key: []string{value}})
|
||||
}
|
||||
|
|
@ -70,6 +70,8 @@ type Config struct {
|
|||
// RtmpIngestURL 是 getGroupCallStreamRtmpUrl 返回给推流端(OBS)的服务器地址,
|
||||
// 形如 "rtmp://<host>:<port>/live"。为空时回落 "rtmp://<AdvertiseIP>:2400/live"。
|
||||
RtmpIngestURL string
|
||||
// PublicBaseURL 是所有客户端可见 telesrv 链接的公开根 URL。
|
||||
PublicBaseURL string
|
||||
// TempKeyResolveCacheTTL 是 PFS temp→perm auth key 解析的进程内缓存有效期。>0 时同一 temp key
|
||||
// 在 TTL 内复用上次解析、跳过每帧 ResolveAuthKey 的 PG 查询;0(默认/测试)关闭=每帧重校验。
|
||||
// 显式撤销会删除协议 auth key、清缓存并断开活跃连接;TTL 只影响自然过期或异常路径下的
|
||||
|
|
@ -186,6 +188,7 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
|||
r.registerUpload(d)
|
||||
r.registerPhotos(d)
|
||||
r.registerFolders(d)
|
||||
r.registerChatlists(d)
|
||||
r.registerContacts(d)
|
||||
r.registerLangpack(d)
|
||||
r.registerStories(d)
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ func (r *Router) onStoriesExportStoryLink(ctx context.Context, req *tg.StoriesEx
|
|||
return nil, storyIDInvalidErr()
|
||||
}
|
||||
}
|
||||
return &tg.ExportedStoryLink{Link: storyExportLink(peer, req.ID)}, nil
|
||||
return &tg.ExportedStoryLink{Link: r.storyExportLink(peer, req.ID)}, nil
|
||||
}
|
||||
|
||||
func validateStoriesExportStoryLinkRequest(req *tg.StoriesExportStoryLinkRequest) error {
|
||||
|
|
@ -3018,8 +3018,8 @@ func uniqueStoryIDs(ids []int) []int {
|
|||
return out
|
||||
}
|
||||
|
||||
func storyExportLink(peer domain.Peer, storyID int) string {
|
||||
return fmt.Sprintf("https://telesrv.local/story/%s/%d/%d", peer.Type, peer.ID, storyID)
|
||||
func (r *Router) storyExportLink(peer domain.Peer, storyID int) string {
|
||||
return r.publicLink(fmt.Sprintf("story/%s/%d/%d", peer.Type, peer.ID, storyID))
|
||||
}
|
||||
|
||||
func (r *Router) recordStoryChange(ctx context.Context, userID int64, story domain.Story) error {
|
||||
|
|
|
|||
|
|
@ -5889,7 +5889,7 @@ func TestStoriesLongtailCompatHandlers(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("export story link: %v", err)
|
||||
}
|
||||
if link.Link != "https://telesrv.local/story/user/9311/1" {
|
||||
if link.Link != "https://telesrv.net/story/user/9311/1" {
|
||||
t.Fatalf("export story link = %q, want deterministic telesrv link", link.Link)
|
||||
}
|
||||
if _, err := r.onStoriesExportStoryLink(reqCtx, &tg.StoriesExportStoryLinkRequest{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue