feat: sync community aggregates
Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates). Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
This commit is contained in:
parent
7de1766941
commit
da6a57e1a3
52 changed files with 5687 additions and 289 deletions
|
|
@ -35,6 +35,12 @@ func (r *Router) notifyScopeFromInput(userID int64, in tg.InputNotifyPeerClass)
|
|||
return domain.NotifyScope{Kind: domain.NotifyScopeChats}, true
|
||||
case *tg.InputNotifyBroadcasts:
|
||||
return domain.NotifyScope{Kind: domain.NotifyScopeBroadcasts}, true
|
||||
case *tg.InputNotifyCommunity:
|
||||
ref, ok := inputChannelRef(p.Community)
|
||||
if !ok {
|
||||
return domain.NotifyScope{}, false
|
||||
}
|
||||
return domain.NotifyScope{Kind: domain.NotifyScopePeer, Peer: domain.Peer{Type: domain.PeerTypeCommunity, ID: ref.ID}}, true
|
||||
case *tg.InputNotifyPeer:
|
||||
peer, ok := r.domainPeerFromInputPeer(userID, p.Peer)
|
||||
if !ok {
|
||||
|
|
@ -145,6 +151,7 @@ func (r *Router) onAccountGetNotifyExceptions(ctx context.Context, req *tg.Accou
|
|||
updates := make([]tg.UpdateClass, 0, len(exceptions))
|
||||
userIDs := make([]int64, 0)
|
||||
channelIDs := make([]int64, 0)
|
||||
communityIDs := make([]int64, 0)
|
||||
for _, ex := range exceptions {
|
||||
if filterPeer != nil && ex.Peer != *filterPeer {
|
||||
continue
|
||||
|
|
@ -163,15 +170,23 @@ func (r *Router) onAccountGetNotifyExceptions(ctx context.Context, req *tg.Accou
|
|||
userIDs = append(userIDs, ex.Peer.ID)
|
||||
case domain.PeerTypeChannel:
|
||||
channelIDs = append(channelIDs, ex.Peer.ID)
|
||||
case domain.PeerTypeCommunity:
|
||||
communityIDs = append(communityIDs, ex.Peer.ID)
|
||||
}
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return empty, nil
|
||||
}
|
||||
chats := r.tgChatsForChannelIDs(ctx, userID, channelIDs)
|
||||
if r.deps.Communities != nil && len(communityIDs) > 0 {
|
||||
if views, err := r.deps.Communities.GetMany(ctx, userID, communityIDs); err == nil {
|
||||
chats = appendUniqueTGChats(chats, tgCommunityChats(views)...)
|
||||
}
|
||||
}
|
||||
return &tg.Updates{
|
||||
Updates: updates,
|
||||
Users: r.tgUsersForIDs(ctx, userID, userIDs),
|
||||
Chats: r.tgChatsForChannelIDs(ctx, userID, channelIDs),
|
||||
Chats: chats,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -260,6 +275,9 @@ func tgNotifyPeer(scope domain.NotifyScope) tg.NotifyPeerClass {
|
|||
case domain.NotifyScopeBroadcasts:
|
||||
return &tg.NotifyBroadcasts{}
|
||||
case domain.NotifyScopePeer:
|
||||
if scope.Peer.Type == domain.PeerTypeCommunity {
|
||||
return &tg.NotifyCommunity{CommunityID: scope.Peer.ID}
|
||||
}
|
||||
peer := tgPeer(scope.Peer)
|
||||
if scope.TopicID != 0 {
|
||||
return &tg.NotifyForumTopic{Peer: peer, TopMsgID: scope.TopicID}
|
||||
|
|
@ -274,7 +292,7 @@ func tgNotifyPeer(scope domain.NotifyScope) tg.NotifyPeerClass {
|
|||
// 显示且跨重启恢复。perf:从 per-user notify 缓存读取(命中即 0 PG),而非每次 getDialogs
|
||||
// 都查 notify_settings——绝大多数用户没有任何自定义静音,缓存命中后零数据库开销。
|
||||
func (r *Router) withDialogNotifySettings(ctx context.Context, viewerUserID int64, list domain.DialogList) domain.DialogList {
|
||||
if len(list.Dialogs) == 0 {
|
||||
if len(list.Dialogs) == 0 && len(list.Communities) == 0 {
|
||||
return list
|
||||
}
|
||||
settings := r.userNotifySettings(ctx, viewerUserID)
|
||||
|
|
@ -287,6 +305,13 @@ func (r *Router) withDialogNotifySettings(ctx context.Context, viewerUserID int6
|
|||
list.Dialogs[i].NotifySettings = &sc
|
||||
}
|
||||
}
|
||||
for i := range list.Communities {
|
||||
peer := domain.Peer{Type: domain.PeerTypeCommunity, ID: list.Communities[i].Community.ID}
|
||||
if s, ok := settings[peer]; ok {
|
||||
sc := s.Clone()
|
||||
list.Communities[i].State.NotifySettings = &sc
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"telesrv/internal/domain"
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) onChannelsCreateChannel(ctx context.Context, req *tg.ChannelsCreateChannelRequest) (tg.UpdatesClass, error) {
|
||||
|
|
@ -71,37 +74,54 @@ func (r *Router) onChannelsGetChannels(ctx context.Context, ids []tg.InputChanne
|
|||
channelIDs := make([]int64, 0, len(ids))
|
||||
for _, input := range ids {
|
||||
ref, ok := inputChannelRef(input)
|
||||
if !ok || ref.ID == 0 || r.deps.Channels == nil {
|
||||
if !ok || ref.ID == 0 {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, ref)
|
||||
channelIDs = append(channelIDs, ref.ID)
|
||||
}
|
||||
if len(channelIDs) == 0 || r.deps.Channels == nil {
|
||||
if len(channelIDs) == 0 || (r.deps.Channels == nil && r.deps.Communities == nil) {
|
||||
return &tg.MessagesChats{}, nil
|
||||
}
|
||||
views, err := r.deps.Channels.GetChannels(ctx, userID, channelIDs)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
var views []domain.ChannelView
|
||||
if r.deps.Channels != nil {
|
||||
views, err = r.deps.Channels.GetChannels(ctx, userID, channelIDs)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
}
|
||||
byID := make(map[int64]domain.ChannelView, len(views))
|
||||
for _, view := range views {
|
||||
byID[view.Channel.ID] = view
|
||||
}
|
||||
communityByID := make(map[int64]domain.CommunityView)
|
||||
if r.deps.Communities != nil {
|
||||
communityViews, err := r.deps.Communities.GetMany(ctx, userID, channelIDs)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
for _, view := range communityViews {
|
||||
communityByID[view.Community.ID] = view
|
||||
}
|
||||
}
|
||||
chats := make([]tg.ChatClass, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
view, ok := byID[ref.ID]
|
||||
if !ok || !inputChannelAccessHashMatches(ref, view.Channel) {
|
||||
if view, ok := communityByID[ref.ID]; ok {
|
||||
if !ref.CheckAccessHash || ref.AccessHash == view.Community.AccessHash {
|
||||
chats = append(chats, tgCommunityChat(view))
|
||||
}
|
||||
continue
|
||||
}
|
||||
chats = append(chats, tgChannelChatForView(userID, view))
|
||||
if view, ok := byID[ref.ID]; ok && inputChannelAccessHashMatches(ref, view.Channel) {
|
||||
chats = append(chats, tgChannelChatForView(userID, view))
|
||||
}
|
||||
}
|
||||
r.applyStoryMaxIDsToPeerObjects(ctx, userID, nil, chats)
|
||||
return &tg.MessagesChats{Chats: chats}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputChannelClass) (*tg.MessagesChatFull, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return &tg.MessagesChatFull{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
@ -112,6 +132,34 @@ func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputCha
|
|||
if !ok {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
if r.deps.Communities != nil {
|
||||
view, communityErrValue := r.deps.Communities.Get(ctx, userID, ref.ID)
|
||||
if communityErrValue == nil {
|
||||
if ref.CheckAccessHash && ref.AccessHash != view.Community.AccessHash {
|
||||
return nil, channelInvalidErr(domain.ErrCommunityPrivate)
|
||||
}
|
||||
if settings := r.userNotifySettings(ctx, userID); len(settings) > 0 {
|
||||
if setting, ok := settings[domain.Peer{Type: domain.PeerTypeCommunity, ID: view.Community.ID}]; ok {
|
||||
copy := setting.Clone()
|
||||
view.State.NotifySettings = ©
|
||||
}
|
||||
}
|
||||
return &tg.MessagesChatFull{
|
||||
FullChat: tgCommunityFull(view),
|
||||
Chats: tgCommunityHydratedChats(userID, view),
|
||||
Users: tgUsers(view.Users),
|
||||
}, nil
|
||||
}
|
||||
if errors.Is(communityErrValue, domain.ErrCommunityPrivate) {
|
||||
return nil, communityErr(communityErrValue)
|
||||
}
|
||||
if !errors.Is(communityErrValue, domain.ErrCommunityInvalid) {
|
||||
return nil, communityErr(communityErrValue)
|
||||
}
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
loadEpoch := r.channelFullProjectionCache.LoadEpoch()
|
||||
if cached, ok := r.channelFullProjectionCache.Lookup(userID, ref.ID); ok {
|
||||
if !inputChannelAccessHashMatches(ref, domain.Channel{ID: ref.ID, AccessHash: cached.accessHash}) {
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ func (r *Router) onMessagesEditChatAdmin(ctx context.Context, req *tg.MessagesEd
|
|||
}
|
||||
|
||||
func (r *Router) onMessagesEditChatAbout(ctx context.Context, req *tg.MessagesEditChatAboutRequest) (bool, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
if utf8.RuneCountInString(req.About) > maxChannelAboutLength {
|
||||
|
|
@ -237,6 +237,22 @@ func (r *Router) onMessagesEditChatAbout(ctx context.Context, req *tg.MessagesEd
|
|||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if community, ok, err := r.maybeCommunityFromInputPeer(ctx, userID, req.Peer); ok {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
view, changed, err := r.deps.Communities.EditAbout(ctx, userID, community.Community.ID, req.About)
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
if changed {
|
||||
r.pushCommunityState(ctx, userID, view)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return false, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -255,13 +271,26 @@ func (r *Router) onMessagesEditChatAbout(ctx context.Context, req *tg.MessagesEd
|
|||
}
|
||||
|
||||
func (r *Router) onMessagesEditChatDefaultBannedRights(ctx context.Context, req *tg.MessagesEditChatDefaultBannedRightsRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if community, ok, err := r.maybeCommunityFromInputPeer(ctx, userID, req.Peer); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, changed, err := r.deps.Communities.EditDefaultBannedRights(ctx, userID, community.Community.ID, domainChannelBannedRights(req.BannedRights))
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
return r.communityMutationUpdates(ctx, userID, view, changed), nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ func (r *Router) onChannelsGetAdminedPublicChannels(ctx context.Context, req *tg
|
|||
if req.ByLocation {
|
||||
return &tg.MessagesChats{}, nil
|
||||
}
|
||||
if req.ForCommunityPeer {
|
||||
channels, err := r.deps.Channels.ListCommunityLinkableChannels(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return &tg.MessagesChats{Chats: tgChannels(userID, channels)}, nil
|
||||
}
|
||||
channels, err := r.deps.Channels.ListAdminedPublicChannels(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
|
|
@ -107,7 +114,7 @@ func (r *Router) onChannelsGetMessageAuthor(ctx context.Context, req *tg.Channel
|
|||
}
|
||||
|
||||
func (r *Router) onChannelsGetParticipants(ctx context.Context, req *tg.ChannelsGetParticipantsRequest) (tg.ChannelsChannelParticipantsClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return &tg.ChannelsChannelParticipants{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
@ -122,6 +129,26 @@ func (r *Router) onChannelsGetParticipants(ctx context.Context, req *tg.Channels
|
|||
if utf8.RuneCountInString(filter.Query) > domain.MaxChannelParticipantsQueryLength {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if community, isCommunity, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); isCommunity {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := r.deps.Communities.Participants(ctx, userID, community.Community.ID, filter, req.Offset, req.Limit)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
if req.Hash != 0 && list.Hash == req.Hash {
|
||||
return &tg.ChannelsChannelParticipantsNotModified{}, nil
|
||||
}
|
||||
participants := make([]tg.ChannelParticipantClass, 0, len(list.Participants))
|
||||
for _, member := range list.Participants {
|
||||
participants = append(participants, tgCommunityMember(userID, member))
|
||||
}
|
||||
return &tg.ChannelsChannelParticipants{Count: list.Count, Participants: participants, Chats: []tg.ChatClass{tgCommunityChat(community)}, Users: tgUsers(list.Users)}, nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
list, err := r.deps.Channels.GetParticipants(ctx, userID, ref.ID, filter, req.Offset, req.Limit)
|
||||
if err != nil {
|
||||
return nil, channelInvalidErr(err)
|
||||
|
|
@ -390,17 +417,13 @@ func (r *Router) recordChannelStateForUser(ctx context.Context, userID, channelI
|
|||
}
|
||||
|
||||
func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAdminRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, found, err := r.userFromInput(ctx, userID, req.UserID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
|
|
@ -408,6 +431,33 @@ func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAd
|
|||
if !found || target.ID == 0 {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, changed, err := r.deps.Communities.EditAdmin(ctx, userID, domain.CommunityEditAdminRequest{
|
||||
CommunityID: community.Community.ID,
|
||||
UserID: target.ID,
|
||||
Rights: domainChannelAdminRights(req.AdminRights),
|
||||
Rank: req.Rank,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
updates := r.communityMutationUpdates(ctx, userID, view, changed)
|
||||
if changed && target.ID != userID {
|
||||
r.refreshAndPushCommunityState(ctx, target.ID, community.Community.ID, community.Community)
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := r.deps.Channels.EditAdmin(ctx, userID, domain.EditChannelAdminRequest{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ func (r *Router) onChannelsToggleAutotranslation(ctx context.Context, req *tg.Ch
|
|||
}
|
||||
|
||||
func (r *Router) onChannelsEditTitle(ctx context.Context, req *tg.ChannelsEditTitleRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if !validChannelTitle(req.Title) {
|
||||
|
|
@ -319,6 +319,19 @@ func (r *Router) onChannelsEditTitle(ctx context.Context, req *tg.ChannelsEditTi
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, changed, err := r.deps.Communities.EditTitle(ctx, userID, community.Community.ID, req.Title)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
return r.communityMutationUpdates(ctx, userID, view, changed), nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -354,7 +367,7 @@ func (r *Router) onChannelsEditTitle(ctx context.Context, req *tg.ChannelsEditTi
|
|||
}
|
||||
|
||||
func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPhotoRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if req.Photo == nil {
|
||||
|
|
@ -364,11 +377,24 @@ func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPh
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
||||
photo, err := r.resolveInputChatPhoto(ctx, userID, req.Photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
photo, err := r.resolveInputChatPhoto(ctx, userID, req.Photo)
|
||||
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, changed, err := r.deps.Communities.SetPhoto(ctx, userID, community.Community.ID, photo, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
return r.communityMutationUpdates(ctx, userID, view, changed), nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,13 +61,29 @@ func (r *Router) onChannelsSetMainProfileTab(ctx context.Context, req *tg.Channe
|
|||
}
|
||||
|
||||
func (r *Router) onChannelsDeleteChannel(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, input); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, _, err := r.deps.Communities.Delete(ctx, userID, community.Community.ID, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
for _, serviceMessage := range view.ServiceMessages {
|
||||
r.enqueueChannelMessageFanout(ctx, userID, serviceMessage, nil)
|
||||
}
|
||||
return r.communityMutationUpdates(ctx, userID, view, true), nil
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
channelID, err := r.channelIDFromInput(ctx, userID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
467
internal/rpc/communities.go
Normal file
467
internal/rpc/communities.go
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func communityErr(err error) error {
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, domain.ErrCommunityPrivate):
|
||||
return tgerr400("CHANNEL_PRIVATE")
|
||||
case errors.Is(err, domain.ErrCommunityAdminRequired):
|
||||
return tgerr400("CHAT_ADMIN_REQUIRED")
|
||||
case errors.Is(err, domain.ErrCommunityCreatorRequired):
|
||||
return tgerr400("CHAT_ADMIN_REQUIRED")
|
||||
case errors.Is(err, domain.ErrCommunityPeersTooMuch):
|
||||
return tgerr400("COMMUNITY_PEERS_TOO_MUCH")
|
||||
case errors.Is(err, domain.ErrCommunityRequestCreated):
|
||||
return tgerr400("COMMUNITY_REQUEST_CREATED")
|
||||
case errors.Is(err, domain.ErrCommunityRequestMissing):
|
||||
return tgerr400("COMMUNITY_REQUEST_MISSING")
|
||||
case errors.Is(err, domain.ErrCommunityPeerLinked):
|
||||
return tgerr400("COMMUNITY_PEER_ALREADY_LINKED")
|
||||
case errors.Is(err, domain.ErrCommunityPeerInvalid), errors.Is(err, domain.ErrCommunityParticipantInvalid):
|
||||
return peerIDInvalidErr()
|
||||
case errors.Is(err, domain.ErrChannelTitleInvalid):
|
||||
return tgerr400("CHAT_TITLE_EMPTY")
|
||||
case errors.Is(err, domain.ErrAboutTooLong):
|
||||
return aboutTooLongErr()
|
||||
case errors.Is(err, domain.ErrCommunityInvalid):
|
||||
return channelInvalidErr(err)
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) communityFromInput(ctx context.Context, userID int64, input tg.InputChannelClass) (domain.CommunityView, error) {
|
||||
if r.deps.Communities == nil {
|
||||
return domain.CommunityView{}, notImplementedErr()
|
||||
}
|
||||
ref, ok := inputChannelRef(input)
|
||||
if !ok || ref.ID == 0 {
|
||||
return domain.CommunityView{}, channelInvalidErr(domain.ErrCommunityInvalid)
|
||||
}
|
||||
view, err := r.deps.Communities.Get(ctx, userID, ref.ID)
|
||||
if err != nil {
|
||||
return domain.CommunityView{}, communityErr(err)
|
||||
}
|
||||
if ref.CheckAccessHash && ref.AccessHash != view.Community.AccessHash {
|
||||
return domain.CommunityView{}, communityErr(domain.ErrCommunityPrivate)
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
// maybeCommunityFromInput distinguishes a Community from an ordinary channel.
|
||||
// IDs share one allocator, so ErrCommunityInvalid is the only fallthrough case.
|
||||
func (r *Router) maybeCommunityFromInput(ctx context.Context, userID int64, input tg.InputChannelClass) (domain.CommunityView, bool, error) {
|
||||
if r.deps.Communities == nil {
|
||||
return domain.CommunityView{}, false, nil
|
||||
}
|
||||
ref, ok := inputChannelRef(input)
|
||||
if !ok || ref.ID == 0 {
|
||||
return domain.CommunityView{}, false, nil
|
||||
}
|
||||
view, err := r.deps.Communities.Get(ctx, userID, ref.ID)
|
||||
if errors.Is(err, domain.ErrCommunityInvalid) {
|
||||
return domain.CommunityView{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.CommunityView{}, true, communityErr(err)
|
||||
}
|
||||
if ref.CheckAccessHash && ref.AccessHash != view.Community.AccessHash {
|
||||
return domain.CommunityView{}, true, communityErr(domain.ErrCommunityPrivate)
|
||||
}
|
||||
return view, true, nil
|
||||
}
|
||||
|
||||
func (r *Router) maybeCommunityFromInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.CommunityView, bool, error) {
|
||||
ref, ok := inputPeerChannelRef(peer)
|
||||
if !ok {
|
||||
return domain.CommunityView{}, false, nil
|
||||
}
|
||||
input := &tg.InputChannel{ChannelID: ref.ID, AccessHash: ref.AccessHash}
|
||||
return r.maybeCommunityFromInput(ctx, userID, input)
|
||||
}
|
||||
|
||||
func (r *Router) communityPeerFromInput(ctx context.Context, userID int64, input tg.InputPeerClass) (domain.Peer, error) {
|
||||
peer, ok := r.domainPeerFromInputPeer(userID, input)
|
||||
if peer.ID == 0 || (peer.Type != domain.PeerTypeChannel && peer.Type != domain.PeerTypeUser) {
|
||||
return domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
if !ok {
|
||||
return domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel {
|
||||
ref, ok := inputPeerChannelRef(input)
|
||||
if !ok || r.deps.Channels == nil {
|
||||
return domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
// A Community admin may approve or unlink a channel without being a
|
||||
// member of that channel. Resolve the immutable base row for constructor
|
||||
// and access_hash validation; aggregate authorization remains in the
|
||||
// Community transaction (direct links still require channel admin rights,
|
||||
// approvals require an existing validated request).
|
||||
if resolver, ok := r.deps.Channels.(interface {
|
||||
GetChannelByID(context.Context, int64) (domain.Channel, error)
|
||||
}); ok {
|
||||
channel, err := resolver.GetChannelByID(ctx, peer.ID)
|
||||
if err != nil || channel.ID == 0 || channel.Deleted {
|
||||
return domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
if ref.CheckAccessHash && !inputChannelAccessHashMatches(ref, channel) {
|
||||
return domain.Peer{}, channelInvalidErr(domain.ErrChannelPrivate)
|
||||
}
|
||||
} else if err := r.validateInputPeerChannelAccess(ctx, userID, input, peer.ID); err != nil {
|
||||
return domain.Peer{}, err
|
||||
}
|
||||
}
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (r *Router) communityUpdates(view domain.CommunityView) *tg.Updates {
|
||||
return &tg.Updates{Updates: []tg.UpdateClass{}, Users: tgUsers(view.Users), Chats: tgCommunityHydratedChats(view.Self.UserID, view), Date: int(r.clock.Now().Unix())}
|
||||
}
|
||||
|
||||
func (r *Router) pushCommunityState(ctx context.Context, userID int64, view domain.CommunityView) {
|
||||
r.pushUserUpdates(ctx, userID, r.communityUpdates(view))
|
||||
}
|
||||
|
||||
func (r *Router) refreshAndPushCommunityState(ctx context.Context, viewerUserID, communityID int64, fallback domain.Community) {
|
||||
if viewerUserID == 0 || r.deps.Communities == nil {
|
||||
return
|
||||
}
|
||||
view, err := r.deps.Communities.Get(ctx, viewerUserID, communityID)
|
||||
if err == nil {
|
||||
r.pushCommunityState(ctx, viewerUserID, view)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, domain.ErrCommunityPrivate) {
|
||||
r.pushCommunityState(ctx, viewerUserID, domain.CommunityView{
|
||||
Community: fallback,
|
||||
Self: domain.CommunityMember{CommunityID: communityID, UserID: viewerUserID},
|
||||
Forbidden: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) communityMutationUpdates(ctx context.Context, userID int64, view domain.CommunityView, changed bool) *tg.Updates {
|
||||
out := r.communityUpdates(view)
|
||||
if changed {
|
||||
r.pushCommunityState(ctx, userID, view)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) withCommunityDialogList(ctx context.Context, userID int64, filter domain.DialogFilter, list domain.DialogList) (domain.DialogList, error) {
|
||||
if r.deps.Communities == nil || (filter.HasFolderID && filter.FolderID != domain.DialogMainFolderID) {
|
||||
return list, nil
|
||||
}
|
||||
views, err := r.deps.Communities.ListJoined(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.DialogList{}, err
|
||||
}
|
||||
for _, view := range views {
|
||||
if !view.State.Collapsed || (filter.PinnedOnly && !view.State.Pinned) || (filter.ExcludePinned && view.State.Pinned) {
|
||||
continue
|
||||
}
|
||||
list.Communities = append(list.Communities, view)
|
||||
list.Count++
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *Router) communityDialogPeerFromInput(ctx context.Context, userID int64, input tg.InputDialogPeerClass) (domain.CommunityView, bool, error) {
|
||||
peer, ok := input.(*tg.InputDialogPeerCommunity)
|
||||
if !ok || peer == nil || peer.Community == nil {
|
||||
return domain.CommunityView{}, false, nil
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, peer.Community)
|
||||
return view, true, err
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesCreate(ctx context.Context, req *tg.CommunitiesCreateRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil || r.deps.Communities == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peer, err := r.communityPeerFromInput(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visibility := domain.CommunityPeerVisible
|
||||
if req.Hidden {
|
||||
visibility = domain.CommunityPeerHidden
|
||||
}
|
||||
view, err := r.deps.Communities.Create(ctx, userID, domain.CreateCommunityRequest{Title: req.Title, About: req.About, InitialPeer: peer, Visibility: visibility, Date: int(r.clock.Now().Unix())})
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
for _, serviceMessage := range view.ServiceMessages {
|
||||
r.enqueueChannelMessageFanout(ctx, userID, serviceMessage, nil)
|
||||
}
|
||||
return r.communityUpdates(view), nil
|
||||
}
|
||||
|
||||
func (r *Router) emitCommunityLinkService(ctx context.Context, actorUserID int64, result domain.CommunityTogglePeerLinkResult) {
|
||||
if result.ServiceMessage == nil {
|
||||
return
|
||||
}
|
||||
r.enqueueChannelMessageFanout(ctx, actorUserID, *result.ServiceMessage, nil)
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesTogglePeerLink(ctx context.Context, req *tg.CommunitiesTogglePeerLinkRequest) (bool, error) {
|
||||
if req == nil || r.deps.Communities == nil {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
actions := 0
|
||||
if req.Visible {
|
||||
actions++
|
||||
}
|
||||
if req.Hidden {
|
||||
actions++
|
||||
}
|
||||
if req.Deleted {
|
||||
actions++
|
||||
}
|
||||
if actions != 1 {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
peer, err := r.communityPeerFromInput(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
visibility := domain.CommunityPeerVisible
|
||||
if req.Hidden {
|
||||
visibility = domain.CommunityPeerHidden
|
||||
}
|
||||
result, err := r.deps.Communities.TogglePeerLink(ctx, userID, domain.CommunityTogglePeerLinkRequest{CommunityID: view.Community.ID, Peer: peer, Visibility: visibility, Deleted: req.Deleted, Date: int(r.clock.Now().Unix())})
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
if result.RequestCreated {
|
||||
return false, tgerr400("COMMUNITY_REQUEST_CREATED")
|
||||
}
|
||||
r.emitCommunityLinkService(ctx, userID, result)
|
||||
r.refreshAndPushCommunityState(ctx, userID, view.Community.ID, result.Community)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesGetJoined(ctx context.Context) (tg.MessagesChatsClass, error) {
|
||||
if r.deps.Communities == nil {
|
||||
return &tg.MessagesChats{}, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
views, err := r.deps.Communities.ListJoined(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
return &tg.MessagesChats{Chats: tgCommunityChats(views)}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesToggleCollapsed(ctx context.Context, req *tg.CommunitiesToggleCommunityCollapsedInDialogsRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wasPinned := view.State.Pinned
|
||||
view, changed, err := r.deps.Communities.SetCollapsed(ctx, userID, view.Community.ID, req.Collapsed)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
out := r.communityUpdates(view)
|
||||
if changed && !req.Collapsed && wasPinned {
|
||||
out.Updates = append(out.Updates, &tg.UpdateDialogPinned{Peer: &tg.DialogPeerCommunity{CommunityID: view.Community.ID}})
|
||||
}
|
||||
if changed {
|
||||
r.pushCommunityState(ctx, userID, view)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesGetPeerLinkRequests(ctx context.Context, req *tg.CommunitiesGetPeerLinkRequestsRequest) (*tg.CommunitiesPeerLinkRequests, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := r.deps.Communities.ListPeerLinkRequests(ctx, userID, view.Community.ID, req.Offset, req.Limit)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
requests := make([]tg.CommunityPeerRequest, 0, len(page.Requests))
|
||||
for _, item := range page.Requests {
|
||||
requests = append(requests, tg.CommunityPeerRequest{Visible: item.Visibility == domain.CommunityPeerVisible, Peer: tgPeer(item.Peer), RequestedBy: item.RequestedBy, Date: item.Date})
|
||||
}
|
||||
out := &tg.CommunitiesPeerLinkRequests{TotalCount: page.TotalCount, Requests: requests, Chats: tgChannels(userID, page.Channels), Users: tgUsers(page.Users)}
|
||||
if page.NextOffset != "" {
|
||||
out.SetNextOffset(page.NextOffset)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesTogglePeerLinkRequestApproval(ctx context.Context, req *tg.CommunitiesTogglePeerLinkRequestApprovalRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
peer, err := r.communityPeerFromInput(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result, err := r.deps.Communities.DecidePeerLinkRequest(ctx, userID, view.Community.ID, peer, req.Reject, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
if !req.Reject {
|
||||
r.emitCommunityLinkService(ctx, userID, result)
|
||||
r.refreshAndPushCommunityState(ctx, userID, view.Community.ID, result.Community)
|
||||
if result.RequestedBy != userID {
|
||||
r.refreshAndPushCommunityState(ctx, result.RequestedBy, view.Community.ID, result.Community)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesToggleAllPeerLinkRequestApproval(ctx context.Context, req *tg.CommunitiesToggleAllPeerLinkRequestApprovalRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
results, err := r.deps.Communities.DecideAllPeerLinkRequests(ctx, userID, view.Community.ID, req.Reject, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
if !req.Reject {
|
||||
requesters := map[int64]struct{}{}
|
||||
for _, result := range results {
|
||||
r.emitCommunityLinkService(ctx, userID, result)
|
||||
if result.RequestedBy != 0 && result.RequestedBy != userID {
|
||||
requesters[result.RequestedBy] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(results) > 0 {
|
||||
r.refreshAndPushCommunityState(ctx, userID, view.Community.ID, results[0].Community)
|
||||
for requester := range requesters {
|
||||
r.refreshAndPushCommunityState(ctx, requester, view.Community.ID, results[0].Community)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesToggleParticipantBanned(ctx context.Context, req *tg.CommunitiesToggleParticipantBannedRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
peer, err := r.communityPeerFromInput(ctx, userID, req.Participant)
|
||||
if err != nil || peer.Type != domain.PeerTypeUser {
|
||||
return false, peerIDInvalidErr()
|
||||
}
|
||||
result, err := r.deps.Communities.ToggleParticipantBanned(ctx, userID, view.Community.ID, peer.ID, req.Unban, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
for _, removed := range result.RemovedLinks {
|
||||
r.emitCommunityLinkService(ctx, userID, removed)
|
||||
}
|
||||
for _, ban := range result.ChannelBans {
|
||||
r.invalidateChannelFullBotInfoCacheForChannel(ban.Channel.ID)
|
||||
r.removeOnlineChannelMemberships(ban.Channel.ID, peer.ID)
|
||||
r.recordChannelStateForUser(ctx, peer.ID, ban.Channel.ID, false)
|
||||
cache := newViewerPeerCache(r)
|
||||
build := func(viewerUserID int64) *tg.Updates {
|
||||
updates := r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, userID, ban.Channel, ban.Previous, ban.Participant, ban.Date, cache)
|
||||
if updates != nil && ban.ServiceEvent.Pts != 0 {
|
||||
if update := tgChannelUpdate(viewerUserID, ban.ServiceEvent); update != nil {
|
||||
updates.Updates = append([]tg.UpdateClass{update}, updates.Updates...)
|
||||
}
|
||||
}
|
||||
return updates
|
||||
}
|
||||
r.pushChannelUpdates(ctx, userID, ban.Channel.ID, ban.Recipients, build)
|
||||
}
|
||||
if result.Changed && !req.Unban {
|
||||
forbidden := domain.CommunityView{Community: view.Community, Forbidden: true, Self: domain.CommunityMember{UserID: peer.ID}}
|
||||
r.pushUserUpdates(ctx, peer.ID, r.communityUpdates(forbidden))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onCommunitiesGetParticipantJoinedChats(ctx context.Context, req *tg.CommunitiesGetParticipantJoinedChatsRequest) (*tg.CommunitiesParticipantJoinedChats, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
view, err := r.communityFromInput(ctx, userID, req.Community)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer, err := r.communityPeerFromInput(ctx, userID, req.Participant)
|
||||
if err != nil || peer.Type != domain.PeerTypeUser {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
joined, err := r.deps.Communities.ParticipantJoinedChats(ctx, userID, view.Community.ID, peer.ID)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
return &tg.CommunitiesParticipantJoinedChats{CreatorChatIDs: joined.CreatorChatIDs, JoinedChatIDs: joined.JoinedChatIDs, Chats: tgChannels(userID, joined.Channels), Users: tgUsers(joined.Users)}, nil
|
||||
}
|
||||
38
internal/rpc/communities_register.go
Normal file
38
internal/rpc/communities_register.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
)
|
||||
|
||||
func (r *Router) registerCommunities(d *tlprofile.Dispatcher) {
|
||||
registerRPC[*tg.CommunitiesCreateRequest](d, tlprofile.SemanticMethodCommunitiesCreate, func(ctx context.Context, req *tg.CommunitiesCreateRequest) (any, error) {
|
||||
return r.onCommunitiesCreate(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesTogglePeerLinkRequest](d, tlprofile.SemanticMethodCommunitiesTogglePeerLink, func(ctx context.Context, req *tg.CommunitiesTogglePeerLinkRequest) (any, error) {
|
||||
return r.onCommunitiesTogglePeerLink(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesGetJoinedCommunitiesRequest](d, tlprofile.SemanticMethodCommunitiesGetJoinedCommunities, func(ctx context.Context, req *tg.CommunitiesGetJoinedCommunitiesRequest) (any, error) {
|
||||
return r.onCommunitiesGetJoined(ctx)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesToggleCommunityCollapsedInDialogsRequest](d, tlprofile.SemanticMethodCommunitiesToggleCommunityCollapsedInDialogs, func(ctx context.Context, req *tg.CommunitiesToggleCommunityCollapsedInDialogsRequest) (any, error) {
|
||||
return r.onCommunitiesToggleCollapsed(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesGetPeerLinkRequestsRequest](d, tlprofile.SemanticMethodCommunitiesGetPeerLinkRequests, func(ctx context.Context, req *tg.CommunitiesGetPeerLinkRequestsRequest) (any, error) {
|
||||
return r.onCommunitiesGetPeerLinkRequests(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesTogglePeerLinkRequestApprovalRequest](d, tlprofile.SemanticMethodCommunitiesTogglePeerLinkRequestApproval, func(ctx context.Context, req *tg.CommunitiesTogglePeerLinkRequestApprovalRequest) (any, error) {
|
||||
return r.onCommunitiesTogglePeerLinkRequestApproval(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesToggleAllPeerLinkRequestApprovalRequest](d, tlprofile.SemanticMethodCommunitiesToggleAllPeerLinkRequestApproval, func(ctx context.Context, req *tg.CommunitiesToggleAllPeerLinkRequestApprovalRequest) (any, error) {
|
||||
return r.onCommunitiesToggleAllPeerLinkRequestApproval(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesToggleParticipantBannedRequest](d, tlprofile.SemanticMethodCommunitiesToggleParticipantBanned, func(ctx context.Context, req *tg.CommunitiesToggleParticipantBannedRequest) (any, error) {
|
||||
return r.onCommunitiesToggleParticipantBanned(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.CommunitiesGetParticipantJoinedChatsRequest](d, tlprofile.SemanticMethodCommunitiesGetParticipantJoinedChats, func(ctx context.Context, req *tg.CommunitiesGetParticipantJoinedChatsRequest) (any, error) {
|
||||
return r.onCommunitiesGetParticipantJoinedChats(ctx, req)
|
||||
})
|
||||
}
|
||||
315
internal/rpc/communities_rpc_test.go
Normal file
315
internal/rpc/communities_rpc_test.go
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appchannels "telesrv/internal/app/channels"
|
||||
appcommunities "telesrv/internal/app/communities"
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
appstories "telesrv/internal/app/stories"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func communityRPCChannel(t *testing.T, service *appchannels.Service, creator domain.User, title string, members ...domain.User) domain.Channel {
|
||||
t.Helper()
|
||||
memberIDs := make([]int64, 0, len(members))
|
||||
for _, member := range members {
|
||||
memberIDs = append(memberIDs, member.ID)
|
||||
}
|
||||
created, err := service.CreateChannel(context.Background(), creator.ID, domain.CreateChannelRequest{
|
||||
CreatorUserID: creator.ID,
|
||||
Title: title,
|
||||
Megagroup: true,
|
||||
MemberUserIDs: memberIDs,
|
||||
Date: 1_800_100_000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel %q: %v", title, err)
|
||||
}
|
||||
return created.Channel
|
||||
}
|
||||
|
||||
func TestCommunityDialogsSharePinnedLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 711, Phone: "15552000011", FirstName: "Pin Owner"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channels := memory.NewChannelStore()
|
||||
channelService := appchannels.NewService(channels)
|
||||
communityService := appcommunities.NewService(memory.NewCommunityStore(users, channels, nil, nil))
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(users), Channels: channelService,
|
||||
Communities: communityService, Dialogs: appdialogs.NewService(memory.NewDialogStore(), channels),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
inputs := make([]*tg.InputChannel, 0, domain.MaxPinnedDialogsMainFolder)
|
||||
for i := 0; i < domain.MaxPinnedDialogsMainFolder; i++ {
|
||||
channel := communityRPCChannel(t, channelService, owner, "Pinned Community Channel")
|
||||
view, err := communityService.Create(ctx, owner.ID, domain.CreateCommunityRequest{
|
||||
Title: "Pinned Community", InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
|
||||
Visibility: domain.CommunityPeerVisible, Date: 1_800_110_000 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create community %d: %v", i, err)
|
||||
}
|
||||
if _, _, err := communityService.SetCollapsed(ctx, owner.ID, view.Community.ID, true); err != nil {
|
||||
t.Fatalf("collapse community %d: %v", i, err)
|
||||
}
|
||||
inputs = append(inputs, &tg.InputChannel{ChannelID: view.Community.ID, AccessHash: view.Community.AccessHash})
|
||||
}
|
||||
for i := 0; i < domain.MaxPinnedDialogsMainFolder-1; i++ {
|
||||
input := inputs[i]
|
||||
toggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeerCommunity{Community: input}}
|
||||
toggle.SetPinned(true)
|
||||
ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), toggle)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("pin community %d = %v, %v", i, ok, err)
|
||||
}
|
||||
}
|
||||
joined, err := communityService.ListJoined(ctx, owner.ID)
|
||||
if err != nil || len(joined) != domain.MaxPinnedDialogsMainFolder {
|
||||
t.Fatalf("joined Communities before ordinary pin = %+v, %v", joined, err)
|
||||
}
|
||||
for i := 0; i < domain.MaxPinnedDialogsMainFolder-1; i++ {
|
||||
if !joined[i].State.Pinned || !joined[i].State.Collapsed {
|
||||
t.Fatalf("joined Community %d state before ordinary pin = %+v", i, joined[i].State)
|
||||
}
|
||||
}
|
||||
ordinary := communityRPCChannel(t, channelService, owner, "Ordinary Pinned Channel")
|
||||
ordinaryToggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerChannel{
|
||||
ChannelID: ordinary.ID, AccessHash: ordinary.AccessHash,
|
||||
}}}
|
||||
ordinaryToggle.SetPinned(true)
|
||||
if ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), ordinaryToggle); err != nil || !ok {
|
||||
t.Fatalf("pin ordinary dialog at shared limit = %v, %v", ok, err)
|
||||
}
|
||||
pinned, err := r.pinnedDialogsList(ctx, owner.ID, domain.DialogMainFolderID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
order := combinedPinnedDialogPeers(pinned)
|
||||
if len(order) != domain.MaxPinnedDialogsMainFolder || order[0] != (domain.Peer{Type: domain.PeerTypeChannel, ID: ordinary.ID}) {
|
||||
t.Fatalf("combined pinned order = %+v (dialogs=%+v communities=%+v count=%d), want ordinary dialog promoted above Communities", order, pinned.Dialogs, pinned.Communities, pinned.Count)
|
||||
}
|
||||
overLimit := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeerCommunity{Community: inputs[len(inputs)-1]}}
|
||||
overLimit.SetPinned(true)
|
||||
if ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), overLimit); err == nil || ok || !tgerr.Is(err, "PINNED_DIALOGS_TOO_MUCH") {
|
||||
t.Fatalf("pin over shared limit = %v, %v", ok, err)
|
||||
}
|
||||
if _, err := r.onMessagesReorderPinnedDialogs(WithUserID(ctx, owner.ID), &tg.MessagesReorderPinnedDialogsRequest{
|
||||
FolderID: domain.DialogArchiveFolderID,
|
||||
Order: []tg.InputDialogPeerClass{&tg.InputDialogPeerCommunity{Community: inputs[0]}},
|
||||
}); err == nil || !tgerr.Is(err, "FOLDER_ID_INVALID") {
|
||||
t.Fatalf("archive Community reorder error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommunitiesRPCLayer228Lifecycle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 701, Phone: "15552000001", FirstName: "Owner"})
|
||||
member, _ := userStore.Create(ctx, domain.User{AccessHash: 702, Phone: "15552000002", FirstName: "Member"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
channelService := appchannels.NewService(channelStore)
|
||||
initial := communityRPCChannel(t, channelService, owner, "Initial", member)
|
||||
communityService := appcommunities.NewService(memory.NewCommunityStore(userStore, channelStore, nil, nil))
|
||||
storyStore := memory.NewStoryStore()
|
||||
if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, ID: 1,
|
||||
Date: 1_800_100_001, ExpireDate: 1_900_100_001, Public: true,
|
||||
}}); err != nil {
|
||||
t.Fatalf("seed owner story: %v", err)
|
||||
}
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: channelService,
|
||||
Communities: communityService,
|
||||
Stories: appstories.NewService(storyStore),
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1_800_100_100, 0)})
|
||||
|
||||
createdResult, err := r.onCommunitiesCreate(WithUserID(ctx, owner.ID), &tg.CommunitiesCreateRequest{
|
||||
Hidden: true,
|
||||
Title: "Official Community",
|
||||
About: "Layer 228",
|
||||
Peer: &tg.InputPeerChannel{ChannelID: initial.ID, AccessHash: initial.AccessHash},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("communities.create: %v", err)
|
||||
}
|
||||
created, ok := createdResult.(*tg.Updates)
|
||||
if !ok || len(created.Chats) != 1 {
|
||||
t.Fatalf("create result = %#v, want Updates with Community", createdResult)
|
||||
}
|
||||
community, ok := created.Chats[0].(*tg.Community)
|
||||
if !ok || community.Title != "Official Community" || !community.Creator {
|
||||
t.Fatalf("create chat = %#v", created.Chats[0])
|
||||
}
|
||||
inputCommunity := &tg.InputChannel{ChannelID: community.ID, AccessHash: community.AccessHash}
|
||||
|
||||
joinedResult, err := r.onCommunitiesGetJoined(WithUserID(ctx, member.ID))
|
||||
if err != nil {
|
||||
t.Fatalf("communities.getJoinedCommunities: %v", err)
|
||||
}
|
||||
joined := joinedResult.(*tg.MessagesChats)
|
||||
if len(joined.Chats) != 1 || joined.Chats[0].(*tg.Community).ID != community.ID {
|
||||
t.Fatalf("joined communities = %+v", joined.Chats)
|
||||
}
|
||||
|
||||
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputCommunity)
|
||||
if err != nil {
|
||||
t.Fatalf("channels.getFullChannel community: %v", err)
|
||||
}
|
||||
communityFull, ok := full.FullChat.(*tg.CommunityFull)
|
||||
if !ok || communityFull.About != "Layer 228" || len(communityFull.LinkedPeers) != 1 || len(full.Chats) != 2 {
|
||||
t.Fatalf("community full = %#v chats=%+v", full.FullChat, full.Chats)
|
||||
}
|
||||
|
||||
collapsedResult, err := r.onCommunitiesToggleCollapsed(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleCommunityCollapsedInDialogsRequest{
|
||||
Collapsed: true,
|
||||
Community: inputCommunity,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("toggle collapsed: %v", err)
|
||||
}
|
||||
collapsed := collapsedResult.(*tg.Updates)
|
||||
if len(collapsed.Chats) == 0 || !collapsed.Chats[0].(*tg.Community).CollapsedInDialogs {
|
||||
t.Fatalf("collapsed updates = %+v", collapsed.Chats)
|
||||
}
|
||||
list, err := r.withCommunityDialogList(ctx, owner.ID, domain.DialogFilter{}, domain.DialogList{})
|
||||
if err != nil || len(list.Communities) != 1 {
|
||||
t.Fatalf("community dialog list = %+v err=%v", list, err)
|
||||
}
|
||||
dialogs := tgMessagesDialogs(owner.ID, list).(*tg.MessagesDialogs)
|
||||
if len(dialogs.Dialogs) != 1 {
|
||||
t.Fatalf("dialogs = %+v, want dialogCommunity", dialogs.Dialogs)
|
||||
}
|
||||
if dialog, ok := dialogs.Dialogs[0].(*tg.DialogCommunity); !ok || dialog.CommunityID != community.ID {
|
||||
t.Fatalf("dialog = %#v, want community %d", dialogs.Dialogs[0], community.ID)
|
||||
}
|
||||
|
||||
ownedOne := communityRPCChannel(t, channelService, member, "Owned One")
|
||||
ownedTwo := communityRPCChannel(t, channelService, member, "Owned Two")
|
||||
requestLink := func(channel domain.Channel) {
|
||||
t.Helper()
|
||||
ok, err := r.onCommunitiesTogglePeerLink(WithUserID(ctx, member.ID), &tg.CommunitiesTogglePeerLinkRequest{
|
||||
Visible: true,
|
||||
Community: inputCommunity,
|
||||
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
})
|
||||
if err == nil || ok || !tgerr.Is(err, "COMMUNITY_REQUEST_CREATED") {
|
||||
t.Fatalf("request link %d = %v, %v", channel.ID, ok, err)
|
||||
}
|
||||
}
|
||||
requestLink(ownedOne)
|
||||
requestLink(ownedTwo)
|
||||
|
||||
requests, err := r.onCommunitiesGetPeerLinkRequests(WithUserID(ctx, owner.ID), &tg.CommunitiesGetPeerLinkRequestsRequest{
|
||||
Community: inputCommunity,
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil || requests.TotalCount != 2 || len(requests.Requests) != 2 {
|
||||
t.Fatalf("peer link requests = %+v err=%v", requests, err)
|
||||
}
|
||||
// The Community owner is deliberately not a member of Owned One. Approval
|
||||
// must use the request's validated ownership rather than ordinary channel
|
||||
// membership access.
|
||||
approved, err := r.onCommunitiesTogglePeerLinkRequestApproval(WithUserID(ctx, owner.ID), &tg.CommunitiesTogglePeerLinkRequestApprovalRequest{
|
||||
Community: inputCommunity,
|
||||
Peer: &tg.InputPeerChannel{ChannelID: ownedOne.ID, AccessHash: ownedOne.AccessHash},
|
||||
})
|
||||
if err != nil || !approved {
|
||||
t.Fatalf("approve peer link = %v, %v", approved, err)
|
||||
}
|
||||
approved, err = r.onCommunitiesToggleAllPeerLinkRequestApproval(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleAllPeerLinkRequestApprovalRequest{Community: inputCommunity})
|
||||
if err != nil || !approved {
|
||||
t.Fatalf("approve all peer links = %v, %v", approved, err)
|
||||
}
|
||||
|
||||
joinedChats, err := r.onCommunitiesGetParticipantJoinedChats(WithUserID(ctx, owner.ID), &tg.CommunitiesGetParticipantJoinedChatsRequest{
|
||||
Community: inputCommunity,
|
||||
Participant: &tg.InputPeerUser{UserID: member.ID, AccessHash: member.AccessHash},
|
||||
})
|
||||
if err != nil || len(joinedChats.JoinedChatIDs) != 3 || len(joinedChats.CreatorChatIDs) != 2 {
|
||||
t.Fatalf("participant joined chats = %+v err=%v", joinedChats, err)
|
||||
}
|
||||
|
||||
participantsResult, err := r.onChannelsGetParticipants(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantsRequest{
|
||||
Channel: inputCommunity,
|
||||
Filter: &tg.ChannelParticipantsSearch{Q: "memBER"},
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("community participants search: %v", err)
|
||||
}
|
||||
participants := participantsResult.(*tg.ChannelsChannelParticipants)
|
||||
if participants.Count != 1 || len(participants.Participants) != 1 {
|
||||
t.Fatalf("community participants = %+v", participants)
|
||||
}
|
||||
adminsResult, err := r.onChannelsGetParticipants(WithUserID(ctx, member.ID), &tg.ChannelsGetParticipantsRequest{
|
||||
Channel: inputCommunity,
|
||||
Filter: &tg.ChannelParticipantsAdmins{},
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ordinary member Community admins: %v", err)
|
||||
}
|
||||
admins := adminsResult.(*tg.ChannelsChannelParticipants)
|
||||
if admins.Count != 1 || len(admins.Participants) != 1 {
|
||||
t.Fatalf("ordinary member Community admins = %+v, want creator", admins)
|
||||
}
|
||||
if _, err := r.onChannelsGetParticipants(WithUserID(ctx, member.ID), &tg.ChannelsGetParticipantsRequest{
|
||||
Channel: inputCommunity,
|
||||
Filter: &tg.ChannelParticipantsBanned{Q: ""},
|
||||
Limit: 100,
|
||||
}); err == nil || !tgerr.Is(err, "CHAT_ADMIN_REQUIRED") {
|
||||
t.Fatalf("ordinary member Community banned list err = %v, want CHAT_ADMIN_REQUIRED", err)
|
||||
}
|
||||
|
||||
recent, err := r.onStoriesGetPeerMaxIDs(WithUserID(ctx, owner.ID), []tg.InputPeerClass{
|
||||
&tg.InputPeerSelf{},
|
||||
&tg.InputPeerChannel{ChannelID: community.ID, AccessHash: community.AccessHash},
|
||||
&tg.InputPeerSelf{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("stories.getPeerMaxIDs with Community slot: %v", err)
|
||||
}
|
||||
if len(recent) != 3 {
|
||||
t.Fatalf("stories.getPeerMaxIDs slots = %d, want 3", len(recent))
|
||||
}
|
||||
for _, index := range []int{0, 2} {
|
||||
if maxID, ok := recent[index].GetMaxID(); !ok || maxID != 1 {
|
||||
t.Fatalf("stories.getPeerMaxIDs[%d] max_id = %d ok=%v, want 1 true", index, maxID, ok)
|
||||
}
|
||||
}
|
||||
if maxID, ok := recent[1].GetMaxID(); ok || maxID != 0 || recent[1].Live {
|
||||
t.Fatalf("stories.getPeerMaxIDs Community slot = %+v, want empty recentStory", recent[1])
|
||||
}
|
||||
if _, err := r.onStoriesGetPeerMaxIDs(WithUserID(ctx, owner.ID), []tg.InputPeerClass{
|
||||
&tg.InputPeerChannel{ChannelID: community.ID, AccessHash: community.AccessHash + 1},
|
||||
}); err == nil || !tgerr.Is(err, "CHANNEL_PRIVATE") {
|
||||
t.Fatalf("stories.getPeerMaxIDs Community wrong hash err = %v, want CHANNEL_PRIVATE", err)
|
||||
}
|
||||
|
||||
banned, err := r.onCommunitiesToggleParticipantBanned(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleParticipantBannedRequest{
|
||||
Community: inputCommunity,
|
||||
Participant: &tg.InputPeerUser{UserID: member.ID, AccessHash: member.AccessHash},
|
||||
})
|
||||
if err != nil || !banned {
|
||||
t.Fatalf("toggle participant banned = %v, %v", banned, err)
|
||||
}
|
||||
joinedResult, err = r.onCommunitiesGetJoined(WithUserID(ctx, member.ID))
|
||||
if err != nil || len(joinedResult.(*tg.MessagesChats).Chats) != 0 {
|
||||
t.Fatalf("banned member joined communities = %#v err=%v", joinedResult, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -297,6 +297,12 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction
|
|||
return &tg.MessageActionSetChatWallPaper{Wallpaper: wallpaper}
|
||||
}
|
||||
return nil
|
||||
case domain.ChannelActionChangeCommunity:
|
||||
out := &tg.MessageActionChangeCommunity{}
|
||||
if action.CommunityID != 0 {
|
||||
out.SetCommunityID(action.CommunityID)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -431,6 +437,9 @@ func tgChannel(viewerUserID int64, ch domain.Channel, self *domain.ChannelMember
|
|||
if ch.LinkedMonoforumID != 0 && ch.BroadcastMessagesAllowed {
|
||||
out.SetLinkedMonoforumID(ch.LinkedMonoforumID)
|
||||
}
|
||||
if ch.LinkedCommunityID != 0 {
|
||||
out.SetLinkedCommunityID(ch.LinkedCommunityID)
|
||||
}
|
||||
if ch.Username != "" {
|
||||
out.SetUsername(ch.Username)
|
||||
out.SetUsernames(tgUsernames(ch.Username))
|
||||
|
|
@ -852,29 +861,30 @@ func domainChannelAdminRights(rights tg.ChatAdminRights) domain.ChannelAdminRigh
|
|||
|
||||
func tgChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedRights {
|
||||
return tg.ChatBannedRights{
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
UntilDate: rights.UntilDate,
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
ManageLinkedPeers: rights.ManageLinkedPeers,
|
||||
UntilDate: rights.UntilDate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -888,29 +898,30 @@ func tgDefaultChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedR
|
|||
|
||||
func domainChannelBannedRights(rights tg.ChatBannedRights) domain.ChannelBannedRights {
|
||||
return domain.ChannelBannedRights{
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
UntilDate: rights.UntilDate,
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
ManageLinkedPeers: rights.ManageLinkedPeers,
|
||||
UntilDate: rights.UntilDate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
104
internal/rpc/convert_communities.go
Normal file
104
internal/rpc/convert_communities.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func tgCommunityPhoto(c domain.Community) tg.ChatPhotoClass {
|
||||
if c.PhotoID == 0 {
|
||||
return &tg.ChatPhotoEmpty{}
|
||||
}
|
||||
out := &tg.ChatPhoto{PhotoID: c.PhotoID, DCID: c.PhotoDCID}
|
||||
if len(c.PhotoStripped) > 0 {
|
||||
out.SetStrippedThumb(c.PhotoStripped)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityFullPhoto(c domain.Community) tg.PhotoClass {
|
||||
if c.PhotoID == 0 {
|
||||
return &tg.PhotoEmpty{}
|
||||
}
|
||||
sizes := syntheticAvatarSizes()
|
||||
if len(c.PhotoStripped) > 0 {
|
||||
sizes = append([]tg.PhotoSizeClass{&tg.PhotoStrippedSize{Type: "i", Bytes: c.PhotoStripped}}, sizes...)
|
||||
}
|
||||
return &tg.Photo{ID: c.PhotoID, DCID: c.PhotoDCID, Sizes: sizes}
|
||||
}
|
||||
|
||||
func tgCommunityChat(view domain.CommunityView) tg.ChatClass {
|
||||
c := view.Community
|
||||
if c.Deleted || view.Forbidden {
|
||||
return &tg.CommunityForbidden{ID: c.ID, AccessHash: c.AccessHash, Title: c.Title}
|
||||
}
|
||||
out := &tg.Community{Creator: view.Creator(), CollapsedInDialogs: view.State.Collapsed, ID: c.ID, Title: c.Title, Photo: tgCommunityPhoto(c), Date: c.Date}
|
||||
out.SetAccessHash(c.AccessHash)
|
||||
if view.Self.Role == domain.CommunityRoleCreator {
|
||||
out.SetAdminRights(tgChatAdminRights(domain.CreatorChannelAdminRights()))
|
||||
} else if view.Self.Role == domain.CommunityRoleAdmin {
|
||||
out.SetAdminRights(tgChatAdminRights(view.Self.AdminRights))
|
||||
}
|
||||
out.SetDefaultBannedRights(tgDefaultChatBannedRights(c.DefaultBannedRights))
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityChats(views []domain.CommunityView) []tg.ChatClass {
|
||||
out := make([]tg.ChatClass, 0, len(views))
|
||||
for _, v := range views {
|
||||
out = append(out, tgCommunityChat(v))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityPeer(link domain.CommunityPeerLink) tg.CommunityPeer {
|
||||
out := tg.CommunityPeer{CanViewHistory: link.CanViewHistory, Peer: tgPeer(link.Peer)}
|
||||
out.SetVisible(link.Visible())
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityFull(view domain.CommunityView) *tg.CommunityFull {
|
||||
links := make([]tg.CommunityPeer, 0, len(view.Links))
|
||||
for _, l := range view.Links {
|
||||
links = append(links, tgCommunityPeer(l))
|
||||
}
|
||||
out := &tg.CommunityFull{ID: view.Community.ID, About: view.Community.About, ChatPhoto: tgCommunityFullPhoto(view.Community), LinkedPeers: links}
|
||||
if view.AdminsCount > 0 {
|
||||
out.SetAdminsCount(view.AdminsCount)
|
||||
}
|
||||
if view.KickedCount > 0 {
|
||||
out.SetKickedCount(view.KickedCount)
|
||||
}
|
||||
if view.PendingRequests > 0 {
|
||||
out.SetPeerLinkRequestsPending(view.PendingRequests)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityHydratedChats(viewerUserID int64, view domain.CommunityView) []tg.ChatClass {
|
||||
out := []tg.ChatClass{tgCommunityChat(view)}
|
||||
for _, ch := range view.Channels {
|
||||
out = appendUniqueTGChats(out, tgChannelChatMin(viewerUserID, ch))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgCommunityMember(viewerUserID int64, m domain.CommunityMember) tg.ChannelParticipantClass {
|
||||
cm := domain.ChannelMember{ChannelID: m.CommunityID, UserID: m.UserID, Status: domain.ChannelMemberActive, Role: domain.ChannelRoleMember, AdminRights: m.AdminRights, Rank: m.Rank, JoinedAt: m.Date}
|
||||
if m.Status == domain.CommunityMemberKicked {
|
||||
cm.Status = domain.ChannelMemberKicked
|
||||
cm.BannedRights = domain.ChannelBannedRights{ViewMessages: true}
|
||||
}
|
||||
switch m.Role {
|
||||
case domain.CommunityRoleCreator:
|
||||
cm.Role = domain.ChannelRoleCreator
|
||||
case domain.CommunityRoleAdmin:
|
||||
cm.Role = domain.ChannelRoleAdmin
|
||||
}
|
||||
return tgChannelParticipant(viewerUserID, cm)
|
||||
}
|
||||
|
||||
func tgCommunityDialog(view domain.CommunityView, notify *domain.PeerNotifySettings) *tg.DialogCommunity {
|
||||
return &tg.DialogCommunity{Pinned: view.State.Pinned, CommunityID: view.Community.ID, NotifySettings: *tgPeerNotifySettings(notify)}
|
||||
}
|
||||
|
|
@ -1,10 +1,56 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type dialogProjection struct {
|
||||
dialog tg.DialogClass
|
||||
pinned bool
|
||||
pinnedOrder int
|
||||
sequence int
|
||||
}
|
||||
|
||||
func projectedDialogs(list domain.DialogList) []tg.DialogClass {
|
||||
items := make([]dialogProjection, 0, len(list.Dialogs)+len(list.Communities))
|
||||
for _, d := range list.Dialogs {
|
||||
if dialog := tgDialog(d); dialog != nil {
|
||||
items = append(items, dialogProjection{dialog: dialog, pinned: d.Pinned, pinnedOrder: d.PinnedOrder, sequence: len(items)})
|
||||
}
|
||||
}
|
||||
for _, community := range list.Communities {
|
||||
items = append(items, dialogProjection{
|
||||
dialog: tgCommunityDialog(community, community.State.NotifySettings), pinned: community.State.Pinned,
|
||||
pinnedOrder: community.State.PinnedOrder, sequence: len(items),
|
||||
})
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].pinned != items[j].pinned {
|
||||
return items[i].pinned
|
||||
}
|
||||
if items[i].pinned && items[i].pinnedOrder != items[j].pinnedOrder {
|
||||
return items[i].pinnedOrder > items[j].pinnedOrder
|
||||
}
|
||||
return items[i].sequence < items[j].sequence
|
||||
})
|
||||
out := make([]tg.DialogClass, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, item.dialog)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCommunityDialogObjects(viewerUserID int64, list domain.DialogList, chats []tg.ChatClass, users []tg.UserClass) ([]tg.ChatClass, []tg.UserClass) {
|
||||
for _, community := range list.Communities {
|
||||
chats = appendUniqueTGChats(chats, tgCommunityHydratedChats(viewerUserID, community)...)
|
||||
users = appendUniqueTGUsers(users, tgUsersForViewer(viewerUserID, community.Users)...)
|
||||
}
|
||||
return chats, users
|
||||
}
|
||||
|
||||
func tgMessagesDialogs(viewerUserID int64, list domain.DialogList) tg.MessagesDialogsClass {
|
||||
dialogs := make([]tg.DialogClass, 0, len(list.Dialogs)+1)
|
||||
// dialogFolder 条目排在最前:TDesktop 据它发现 archive folder 并渲染
|
||||
|
|
@ -12,11 +58,7 @@ func tgMessagesDialogs(viewerUserID int64, list domain.DialogList) tg.MessagesDi
|
|||
if folder := tgDialogFolder(list.ArchiveSummary); folder != nil {
|
||||
dialogs = append(dialogs, folder)
|
||||
}
|
||||
for _, d := range list.Dialogs {
|
||||
if dialog := tgDialog(d); dialog != nil {
|
||||
dialogs = append(dialogs, dialog)
|
||||
}
|
||||
}
|
||||
dialogs = append(dialogs, projectedDialogs(list)...)
|
||||
messages := make([]tg.MessageClass, 0, len(list.Messages))
|
||||
for _, msg := range list.Messages {
|
||||
if item := tgMessage(msg); item != nil {
|
||||
|
|
@ -30,6 +72,7 @@ func tgMessagesDialogs(viewerUserID int64, list domain.DialogList) tg.MessagesDi
|
|||
}
|
||||
users := tgUsersForViewer(viewerUserID, list.Users)
|
||||
chats := tgChannelsForDialogs(viewerUserID, list.Channels, list.Dialogs)
|
||||
chats, users = appendCommunityDialogObjects(viewerUserID, list, chats, users)
|
||||
if list.Count > len(dialogs) {
|
||||
return &tg.MessagesDialogsSlice{
|
||||
Count: list.Count,
|
||||
|
|
@ -61,11 +104,7 @@ func tgPeerDialogs(viewerUserID int64, list domain.DialogList, st domain.UpdateS
|
|||
if folder := tgDialogFolder(list.ArchiveSummary); folder != nil {
|
||||
out.Dialogs = append(out.Dialogs, folder)
|
||||
}
|
||||
for _, d := range list.Dialogs {
|
||||
if dialog := tgDialog(d); dialog != nil {
|
||||
out.Dialogs = append(out.Dialogs, dialog)
|
||||
}
|
||||
}
|
||||
out.Dialogs = append(out.Dialogs, projectedDialogs(list)...)
|
||||
for _, msg := range list.Messages {
|
||||
if item := tgMessage(msg); item != nil {
|
||||
out.Messages = append(out.Messages, item)
|
||||
|
|
@ -84,6 +123,7 @@ func tgPeerDialogs(viewerUserID int64, list domain.DialogList, st domain.UpdateS
|
|||
}
|
||||
}
|
||||
out.Chats = append(out.Chats, tgChannelsForDialogs(viewerUserID, list.Channels, list.Dialogs)...)
|
||||
out.Chats, out.Users = appendCommunityDialogObjects(viewerUserID, list, out.Chats, out.Users)
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +244,9 @@ func tgDraftWebPage(webpage *domain.DialogDraftWebPage) tg.InputMediaClass {
|
|||
}
|
||||
|
||||
func tgDialogPeer(p domain.Peer) tg.DialogPeerClass {
|
||||
if p.Type == domain.PeerTypeCommunity && p.ID > 0 {
|
||||
return &tg.DialogPeerCommunity{CommunityID: p.ID}
|
||||
}
|
||||
peer := tgPeer(p)
|
||||
if peer == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ func tgSelfUser(u domain.User) *tg.User {
|
|||
applyTgUserBotFields(out, u)
|
||||
applyTgUserPremiumFields(out, u)
|
||||
applyTgUserColorFields(out, u)
|
||||
if u.LinkedCommunityID != 0 {
|
||||
out.SetLinkedCommunityID(u.LinkedCommunityID)
|
||||
}
|
||||
if photo := tgUserProfilePhoto(u); photo != nil {
|
||||
out.Photo = photo
|
||||
}
|
||||
|
|
@ -57,6 +60,9 @@ func tgUser(u domain.User) *tg.User {
|
|||
applyTgUserBotFields(out, u)
|
||||
applyTgUserPremiumFields(out, u)
|
||||
applyTgUserColorFields(out, u)
|
||||
if u.LinkedCommunityID != 0 {
|
||||
out.SetLinkedCommunityID(u.LinkedCommunityID)
|
||||
}
|
||||
if photo := tgUserProfilePhoto(u); photo != nil {
|
||||
out.Photo = photo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -620,6 +620,7 @@ type ChannelsService interface {
|
|||
CheckUsername(ctx context.Context, userID, channelID int64, username string) (bool, error)
|
||||
UpdateUsername(ctx context.Context, userID int64, req domain.UpdateChannelUsernameRequest) (domain.Channel, error)
|
||||
ListAdminedPublicChannels(ctx context.Context, userID int64) ([]domain.Channel, error)
|
||||
ListCommunityLinkableChannels(ctx context.Context, userID int64) ([]domain.Channel, error)
|
||||
ListStoryPostableChannels(ctx context.Context, userID int64) ([]domain.Channel, error)
|
||||
ListSendAsChannels(ctx context.Context, userID int64) ([]domain.Channel, error)
|
||||
ResolvePublicUsername(ctx context.Context, userID int64, username string) (domain.Channel, bool, error)
|
||||
|
|
@ -733,6 +734,32 @@ type ChannelsService interface {
|
|||
FilterActiveMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)
|
||||
}
|
||||
|
||||
// CommunitiesService abstracts the Layer 228 Community aggregation domain.
|
||||
// Community containers never expose tg types and never own message/read/pts state.
|
||||
type CommunitiesService interface {
|
||||
Create(ctx context.Context, userID int64, req domain.CreateCommunityRequest) (domain.CommunityView, error)
|
||||
Get(ctx context.Context, userID, communityID int64) (domain.CommunityView, error)
|
||||
GetMany(ctx context.Context, userID int64, ids []int64) ([]domain.CommunityView, error)
|
||||
ListJoined(ctx context.Context, userID int64) ([]domain.CommunityView, error)
|
||||
TogglePeerLink(ctx context.Context, userID int64, req domain.CommunityTogglePeerLinkRequest) (domain.CommunityTogglePeerLinkResult, error)
|
||||
SetCollapsed(ctx context.Context, userID, communityID int64, collapsed bool) (domain.CommunityView, bool, error)
|
||||
ListPeerLinkRequests(ctx context.Context, userID, communityID int64, offset string, limit int) (domain.CommunityPeerLinkRequestPage, error)
|
||||
DecidePeerLinkRequest(ctx context.Context, userID, communityID int64, peer domain.Peer, reject bool, date int) (domain.CommunityTogglePeerLinkResult, error)
|
||||
DecideAllPeerLinkRequests(ctx context.Context, userID, communityID int64, reject bool, date int) ([]domain.CommunityTogglePeerLinkResult, error)
|
||||
ToggleParticipantBanned(ctx context.Context, userID, communityID, participantUserID int64, unban bool, date int) (domain.CommunityParticipantBanResult, error)
|
||||
ParticipantJoinedChats(ctx context.Context, userID, communityID, participantUserID int64) (domain.CommunityParticipantJoinedChats, error)
|
||||
Participants(ctx context.Context, userID, communityID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.CommunityParticipantList, error)
|
||||
EditTitle(ctx context.Context, userID, communityID int64, title string) (domain.CommunityView, bool, error)
|
||||
EditAbout(ctx context.Context, userID, communityID int64, about string) (domain.CommunityView, bool, error)
|
||||
EditAdmin(ctx context.Context, userID int64, req domain.CommunityEditAdminRequest) (domain.CommunityView, bool, error)
|
||||
EditDefaultBannedRights(ctx context.Context, userID, communityID int64, rights domain.ChannelBannedRights) (domain.CommunityView, bool, error)
|
||||
SetPhoto(ctx context.Context, userID, communityID int64, photo *domain.Photo, date int) (domain.CommunityView, bool, error)
|
||||
Delete(ctx context.Context, userID, communityID int64, date int) (domain.CommunityView, []domain.Peer, error)
|
||||
SetPinned(ctx context.Context, userID, communityID int64, pinned bool) (bool, error)
|
||||
ReorderPinned(ctx context.Context, userID int64, order []domain.Peer, force bool) (bool, error)
|
||||
SearchScope(ctx context.Context, userID, communityID int64) (domain.CommunitySearchScope, error)
|
||||
}
|
||||
|
||||
// FilesService 抽象文件上传分片、下载与媒体(document/photo)组装。
|
||||
// 方法只用 domain 类型;rpc 层负责 tg.InputFileLocation / InputMedia ↔ domain 转换。
|
||||
type FilesService interface {
|
||||
|
|
@ -857,6 +884,7 @@ type Deps struct {
|
|||
Translation TranslationService
|
||||
Stories StoriesService
|
||||
Channels ChannelsService
|
||||
Communities CommunitiesService
|
||||
Files FilesService
|
||||
Bots BotsService
|
||||
Polls PollsService
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) pinnedDialogsList(ctx context.Context, userID int64, folderID int) (domain.DialogList, error) {
|
||||
if r == nil || r.deps.Dialogs == nil {
|
||||
if r == nil {
|
||||
return domain.DialogList{}, nil
|
||||
}
|
||||
key := fmt.Sprintf("%d:%d", userID, folderID)
|
||||
value, err, _ := r.dialogsPinnedSF.Do(key, func() (any, error) {
|
||||
if r.deps.Dialogs == nil {
|
||||
return domain.DialogList{}, nil
|
||||
}
|
||||
return r.deps.Dialogs.GetDialogs(ctx, userID, domain.DialogFilter{
|
||||
PinnedOnly: true,
|
||||
HasFolderID: true,
|
||||
|
|
@ -24,7 +28,93 @@ func (r *Router) pinnedDialogsList(ctx context.Context, userID int64, folderID i
|
|||
return domain.DialogList{}, err
|
||||
}
|
||||
if list, ok := value.(domain.DialogList); ok {
|
||||
return list, nil
|
||||
return r.withCommunityDialogList(ctx, userID, domain.DialogFilter{PinnedOnly: true, HasFolderID: true, FolderID: folderID}, list)
|
||||
}
|
||||
return domain.DialogList{}, nil
|
||||
}
|
||||
|
||||
// combinedPinnedDialogPeers merges ordinary dialogs and collapsed Communities
|
||||
// by their shared server order. The two persistence implementations deliberately
|
||||
// store their own rows, but Layer 228 exposes one messages.getPinnedDialogs list.
|
||||
func combinedPinnedDialogPeers(list domain.DialogList) []domain.Peer {
|
||||
type item struct {
|
||||
peer domain.Peer
|
||||
order int
|
||||
sequence int
|
||||
}
|
||||
items := make([]item, 0, len(list.Dialogs)+len(list.Communities))
|
||||
seen := make(map[domain.Peer]struct{}, cap(items))
|
||||
appendItem := func(peer domain.Peer, pinned bool, order int) {
|
||||
if !pinned || peer.ID == 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[peer]; ok {
|
||||
return
|
||||
}
|
||||
seen[peer] = struct{}{}
|
||||
items = append(items, item{peer: peer, order: order, sequence: len(items)})
|
||||
}
|
||||
for _, dialog := range list.Dialogs {
|
||||
appendItem(dialog.Peer, dialog.Pinned, dialog.PinnedOrder)
|
||||
}
|
||||
for _, community := range list.Communities {
|
||||
appendItem(domain.Peer{Type: domain.PeerTypeCommunity, ID: community.Community.ID}, community.State.Pinned, community.State.PinnedOrder)
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].order != items[j].order {
|
||||
return items[i].order > items[j].order
|
||||
}
|
||||
return items[i].sequence < items[j].sequence
|
||||
})
|
||||
out := make([]domain.Peer, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, item.peer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) ensureCombinedPinCapacity(ctx context.Context, userID int64, folderID int, peer domain.Peer) error {
|
||||
list, err := r.pinnedDialogsList(ctx, userID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peers := combinedPinnedDialogPeers(list)
|
||||
for _, pinned := range peers {
|
||||
if pinned == peer {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(peers) >= domain.PinnedDialogsLimit(folderID, r.userIsPremium(ctx, userID)) {
|
||||
return domain.ErrPinnedDialogsTooMuch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// promoteCombinedPinnedDialog assigns one collision-free order across ordinary
|
||||
// dialogs and Communities. It is called after the underlying row is pinned so
|
||||
// both stores can project the same mixed order without owning each other's data.
|
||||
func (r *Router) promoteCombinedPinnedDialog(ctx context.Context, userID int64, folderID int, peer domain.Peer) error {
|
||||
list, err := r.pinnedDialogsList(ctx, userID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := combinedPinnedDialogPeers(list)
|
||||
order := make([]domain.Peer, 0, len(current)+1)
|
||||
order = append(order, peer)
|
||||
for _, candidate := range current {
|
||||
if candidate != peer {
|
||||
order = append(order, candidate)
|
||||
}
|
||||
}
|
||||
if r.deps.Dialogs != nil {
|
||||
if _, err := r.deps.Dialogs.ReorderPinned(ctx, userID, folderID, order, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if folderID == domain.DialogMainFolderID && r.deps.Communities != nil {
|
||||
if _, err := r.deps.Communities.ReorderPinned(ctx, userID, order, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -573,6 +573,50 @@ func (r *Router) onMessagesToggleDialogPin(ctx context.Context, req *tg.Messages
|
|||
if folderPeer, ok := req.Peer.(*tg.InputDialogPeerFolder); ok {
|
||||
return r.toggleArchiveFolderPin(ctx, userID, folderPeer.FolderID, req.GetPinned())
|
||||
}
|
||||
if community, ok, err := r.communityDialogPeerFromInput(ctx, userID, req.Peer); ok {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
pinned := req.GetPinned()
|
||||
peer := domain.Peer{Type: domain.PeerTypeCommunity, ID: community.Community.ID}
|
||||
if pinned && !community.State.Pinned {
|
||||
if err := r.ensureCombinedPinCapacity(ctx, userID, domain.DialogMainFolderID, peer); err != nil {
|
||||
if errors.Is(err, domain.ErrPinnedDialogsTooMuch) {
|
||||
return false, pinnedTooMuchErr()
|
||||
}
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
changed, err := r.deps.Communities.SetPinned(ctx, userID, community.Community.ID, pinned)
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
if !changed {
|
||||
return true, nil
|
||||
}
|
||||
if pinned {
|
||||
if err := r.promoteCombinedPinnedDialog(ctx, userID, domain.DialogMainFolderID, peer); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
date := int(r.clock.Now().Unix())
|
||||
var recorded domain.UpdateEvent
|
||||
if r.deps.Updates != nil {
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
event, state, err := r.deps.Updates.RecordDialogPinned(ctx, authKeyID, userID, peer, pinned, domain.DialogMainFolderID, rawAuthKeyIDForOrigin(ctx), sessionID)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
date, recorded = state.Date, event
|
||||
}
|
||||
r.bookkeepAuxPtsForCurrentSession(ctx, recorded)
|
||||
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
|
||||
Updates: appendAuxPtsBookkeeping([]tg.UpdateClass{&tg.UpdateDialogPinned{Pinned: pinned, Peer: tgDialogPeer(peer)}}, recorded),
|
||||
Chats: []tg.ChatClass{tgCommunityChat(community)}, Date: date,
|
||||
})
|
||||
return true, nil
|
||||
}
|
||||
peers, err := r.dialogPeersFromInput(ctx, userID, []tg.InputDialogPeerClass{req.Peer})
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -584,6 +628,25 @@ func (r *Router) onMessagesToggleDialogPin(ctx context.Context, req *tg.Messages
|
|||
if r.deps.Dialogs == nil {
|
||||
return true, nil
|
||||
}
|
||||
if pinned {
|
||||
folderID := domain.DialogMainFolderID
|
||||
current, err := r.deps.Dialogs.GetPeerDialogs(ctx, userID, peers)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
for _, dialog := range current.Dialogs {
|
||||
if dialog.Peer == peers[0] {
|
||||
folderID = dialog.FolderID
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := r.ensureCombinedPinCapacity(ctx, userID, folderID, peers[0]); err != nil {
|
||||
if errors.Is(err, domain.ErrPinnedDialogsTooMuch) {
|
||||
return false, pinnedTooMuchErr()
|
||||
}
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
changed, folderID, err := r.deps.Dialogs.TogglePinned(ctx, userID, peers[0], pinned)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrPinnedDialogsTooMuch) {
|
||||
|
|
@ -592,6 +655,11 @@ func (r *Router) onMessagesToggleDialogPin(ctx context.Context, req *tg.Messages
|
|||
return false, internalErr()
|
||||
}
|
||||
if changed {
|
||||
if pinned {
|
||||
if err := r.promoteCombinedPinnedDialog(ctx, userID, folderID, peers[0]); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
date := int(r.clock.Now().Unix())
|
||||
var recorded domain.UpdateEvent
|
||||
if r.deps.Updates != nil {
|
||||
|
|
@ -680,12 +748,36 @@ func (r *Router) onMessagesReorderPinnedDialogs(ctx context.Context, req *tg.Mes
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if r.deps.Dialogs == nil {
|
||||
seen := make(map[domain.Peer]struct{}, len(peers))
|
||||
for _, peer := range peers {
|
||||
if _, duplicate := seen[peer]; duplicate {
|
||||
return false, peerIDInvalidErr()
|
||||
}
|
||||
seen[peer] = struct{}{}
|
||||
if req.FolderID != domain.DialogMainFolderID && peer.Type == domain.PeerTypeCommunity {
|
||||
return false, folderIDInvalidErr()
|
||||
}
|
||||
}
|
||||
if len(peers) > domain.PinnedDialogsLimit(req.FolderID, r.userIsPremium(ctx, userID)) {
|
||||
return false, pinnedTooMuchErr()
|
||||
}
|
||||
if r.deps.Dialogs == nil && r.deps.Communities == nil {
|
||||
return true, nil
|
||||
}
|
||||
changed, err := r.deps.Dialogs.ReorderPinned(ctx, userID, req.FolderID, peers, req.GetForce())
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
changed := false
|
||||
if r.deps.Dialogs != nil {
|
||||
dialogsChanged, err := r.deps.Dialogs.ReorderPinned(ctx, userID, req.FolderID, peers, req.GetForce())
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
changed = dialogsChanged
|
||||
}
|
||||
if r.deps.Communities != nil && req.FolderID == domain.DialogMainFolderID {
|
||||
communitiesChanged, err := r.deps.Communities.ReorderPinned(ctx, userID, peers, req.GetForce())
|
||||
if err != nil {
|
||||
return false, communityErr(err)
|
||||
}
|
||||
changed = changed || communitiesChanged
|
||||
}
|
||||
if !changed {
|
||||
return true, nil
|
||||
|
|
@ -884,6 +976,15 @@ func (r *Router) dialogPeersFromInput(ctx context.Context, userID int64, items [
|
|||
hasFolder := false
|
||||
for _, item := range items {
|
||||
switch p := item.(type) {
|
||||
case *tg.InputDialogPeerCommunity:
|
||||
view, ok, err := r.communityDialogPeerFromInput(ctx, userID, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, inputConstructorInvalidErr()
|
||||
}
|
||||
peers = append(peers, domain.Peer{Type: domain.PeerTypeCommunity, ID: view.Community.ID})
|
||||
case *tg.InputDialogPeer:
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, p.Peer)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -528,19 +528,17 @@ func (r *Router) onMessagesGetRichMessage(ctx context.Context, req *tg.MessagesG
|
|||
}
|
||||
|
||||
func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSearchGlobalRequest) (tg.MessagesMessagesClass, error) {
|
||||
// Layer 228 adds an optional community scope. telesrv has no Communities
|
||||
// membership/link read model yet, so treating this as an ordinary global
|
||||
// search would leak results outside the requested scope. Reject it before
|
||||
// any search/store work until that model exists.
|
||||
if _, ok := req.GetCommunity(); ok || req.Community != nil {
|
||||
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
||||
}
|
||||
if req.BroadcastsOnly && req.GroupsOnly {
|
||||
return &tg.MessagesMessages{}, nil
|
||||
}
|
||||
query := normalizeSearchQuery(req.Q)
|
||||
musicOnly := messagesSearchFilterMusic(req.Filter)
|
||||
if query == "" && !musicOnly {
|
||||
communityInput, hasCommunity := req.GetCommunity()
|
||||
if !hasCommunity && req.Community != nil {
|
||||
communityInput, hasCommunity = req.Community, true
|
||||
}
|
||||
emptyCommunitySearch := query == "" && !musicOnly && hasCommunity && messagesSearchFilterEmpty(req.Filter)
|
||||
if query == "" && !musicOnly && !emptyCommunitySearch {
|
||||
return nil, searchQueryEmptyErr()
|
||||
}
|
||||
if utf8.RuneCountInString(query) > maxMessageSearchQLength {
|
||||
|
|
@ -553,6 +551,19 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
var communityView *domain.CommunityView
|
||||
var communityScope domain.CommunitySearchScope
|
||||
if hasCommunity {
|
||||
view, err := r.communityFromInput(ctx, userID, communityInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := r.deps.Communities.SearchScope(ctx, userID, view.Community.ID)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
communityView, communityScope = &view, scope
|
||||
}
|
||||
limit := req.Limit
|
||||
if limit <= 0 || limit > domain.MaxChannelGlobalSearchLimit {
|
||||
limit = domain.MaxChannelGlobalSearchLimit
|
||||
|
|
@ -565,6 +576,9 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if emptyCommunitySearch {
|
||||
return appendCommunitySearchChat(&tg.MessagesMessages{}, communityView), nil
|
||||
}
|
||||
var private domain.MessageList
|
||||
if !req.BroadcastsOnly && !req.GroupsOnly && r.deps.Messages != nil {
|
||||
filter := domain.MessageFilter{
|
||||
|
|
@ -574,6 +588,10 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
|
|||
Limit: limit + 1,
|
||||
MusicOnly: musicOnly,
|
||||
}
|
||||
if communityView != nil {
|
||||
filter.RestrictPeerIDs = true
|
||||
filter.PeerIDs = communityScope.BotUserIDs
|
||||
}
|
||||
if req.MaxDate > 0 {
|
||||
filter.OffsetDate = req.MaxDate
|
||||
}
|
||||
|
|
@ -586,30 +604,49 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
|
|||
}
|
||||
}
|
||||
if req.UsersOnly || r.deps.Channels == nil {
|
||||
return tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), nil
|
||||
return appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView), nil
|
||||
}
|
||||
channelHistory, err := r.deps.Channels.SearchJoinedMessages(ctx, userID, domain.ChannelGlobalSearchRequest{
|
||||
Query: query,
|
||||
BroadcastsOnly: req.BroadcastsOnly,
|
||||
GroupsOnly: req.GroupsOnly,
|
||||
MusicOnly: musicOnly,
|
||||
HasFolderID: hasFolderID,
|
||||
FolderID: folderID,
|
||||
OffsetRate: req.OffsetRate,
|
||||
OffsetChannelID: channelOffsetID,
|
||||
OffsetID: req.OffsetID,
|
||||
MinDate: req.MinDate,
|
||||
MaxDate: req.MaxDate,
|
||||
Limit: limit,
|
||||
Query: query,
|
||||
ChannelIDs: communityScope.ChannelIDs,
|
||||
RestrictChannelIDs: communityView != nil,
|
||||
AllowPublicPreview: communityView != nil,
|
||||
BroadcastsOnly: req.BroadcastsOnly,
|
||||
GroupsOnly: req.GroupsOnly,
|
||||
MusicOnly: musicOnly,
|
||||
HasFolderID: hasFolderID,
|
||||
FolderID: folderID,
|
||||
OffsetRate: req.OffsetRate,
|
||||
OffsetChannelID: channelOffsetID,
|
||||
OffsetID: req.OffsetID,
|
||||
MinDate: req.MinDate,
|
||||
MaxDate: req.MaxDate,
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, channelInvalidErr(err)
|
||||
}
|
||||
channelHistory = r.enrichChannelHistory(ctx, userID, channelHistory)
|
||||
if req.BroadcastsOnly || req.GroupsOnly {
|
||||
return r.tgGlobalChannelMessages(ctx, userID, limitChannelHistory(channelHistory, limit)), nil
|
||||
return appendCommunitySearchChat(r.tgGlobalChannelMessages(ctx, userID, limitChannelHistory(channelHistory, limit)), communityView), nil
|
||||
}
|
||||
return r.tgGlobalSearchMessages(ctx, userID, limit, private, channelHistory), nil
|
||||
return appendCommunitySearchChat(r.tgGlobalSearchMessages(ctx, userID, limit, private, channelHistory), communityView), nil
|
||||
}
|
||||
|
||||
func appendCommunitySearchChat(result tg.MessagesMessagesClass, view *domain.CommunityView) tg.MessagesMessagesClass {
|
||||
if result == nil || view == nil {
|
||||
return result
|
||||
}
|
||||
chat := tgCommunityChat(*view)
|
||||
switch out := result.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
out.Chats = appendUniqueTGChats(out.Chats, chat)
|
||||
case *tg.MessagesMessagesSlice:
|
||||
out.Chats = appendUniqueTGChats(out.Chats, chat)
|
||||
case *tg.MessagesChannelMessages:
|
||||
out.Chats = appendUniqueTGChats(out.Chats, chat)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func limitMessageList(list domain.MessageList, limit int) domain.MessageList {
|
||||
|
|
@ -791,6 +828,11 @@ func messagesSearchFilterMusic(filter tg.MessagesFilterClass) bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
func messagesSearchFilterEmpty(filter tg.MessagesFilterClass) bool {
|
||||
_, ok := filter.(*tg.InputMessagesFilterEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func messagesSearchFilterChatPhotos(filter tg.MessagesFilterClass) bool {
|
||||
_, ok := filter.(*tg.InputMessagesFilterChatPhotos)
|
||||
return ok
|
||||
|
|
|
|||
|
|
@ -11,24 +11,127 @@ import (
|
|||
"go.uber.org/zap/zaptest"
|
||||
"strings"
|
||||
appchannels "telesrv/internal/app/channels"
|
||||
appcommunities "telesrv/internal/app/communities"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMessagesSearchGlobalRejectsUnsupportedCommunityScope(t *testing.T) {
|
||||
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
func TestMessagesSearchGlobalRestrictsCommunityScope(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 84, Phone: "15550000084", FirstName: "Owner"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
viewer, err := users.Create(ctx, domain.User{AccessHash: 85, Phone: "15550000085", FirstName: "Viewer"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channels := memory.NewChannelStore()
|
||||
channelService := appchannels.NewService(channels)
|
||||
linked, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Linked", Megagroup: true, MemberUserIDs: []int64{viewer.ID}, Date: 100})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicPreview, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Public Preview", Megagroup: true, Date: 101})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicPreview.Channel, err = channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
|
||||
UserID: owner.ID, ChannelID: publicPreview.Channel.ID, Username: "community_public_preview",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outside, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Outside", Megagroup: true, Date: 101})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
communityService := appcommunities.NewService(memory.NewCommunityStore(users, channels, nil, nil))
|
||||
community, err := communityService.Create(ctx, owner.ID, domain.CreateCommunityRequest{
|
||||
Title: "Scope", InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: linked.Channel.ID},
|
||||
Visibility: domain.CommunityPeerVisible, Date: 102,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := communityService.TogglePeerLink(ctx, owner.ID, domain.CommunityTogglePeerLinkRequest{
|
||||
CommunityID: community.Community.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: publicPreview.Channel.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 103,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := New(Config{}, Deps{Users: appusers.NewService(users), Channels: channelService, Communities: communityService}, zaptest.NewLogger(t), clock.System)
|
||||
for i, channel := range []domain.Channel{linked.Channel, publicPreview.Channel, outside.Channel} {
|
||||
_, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
|
||||
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, Message: "scoped result", RandomID: int64(9000 + i),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send channel %d: %v", channel.ID, err)
|
||||
}
|
||||
}
|
||||
req := &tg.MessagesSearchGlobalRequest{
|
||||
Q: "scoped",
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
OffsetPeer: &tg.InputPeerEmpty{},
|
||||
Limit: 20,
|
||||
}
|
||||
req.SetCommunity(&tg.InputChannel{ChannelID: 42, AccessHash: 84})
|
||||
req.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash})
|
||||
|
||||
if _, err := r.onMessagesSearchGlobal(WithUserID(context.Background(), 1000000001), req); !tgerr.Is(err, "CHANNEL_INVALID") {
|
||||
t.Fatalf("community-scoped messages.searchGlobal err = %v, want CHANNEL_INVALID", err)
|
||||
result, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), req)
|
||||
if err != nil {
|
||||
t.Fatalf("community-scoped messages.searchGlobal: %v", err)
|
||||
}
|
||||
response, ok := result.(*tg.MessagesMessages)
|
||||
if !ok || len(response.Messages) != 2 {
|
||||
t.Fatalf("community search result = %#v, want joined and public-preview linked messages", result)
|
||||
}
|
||||
gotChannels := map[int64]bool{}
|
||||
for _, item := range response.Messages {
|
||||
message, ok := item.(*tg.Message)
|
||||
if !ok {
|
||||
t.Fatalf("community search message = %#v, want channel message", item)
|
||||
}
|
||||
peer, ok := message.PeerID.(*tg.PeerChannel)
|
||||
if !ok {
|
||||
t.Fatalf("community search message peer = %#v", message.PeerID)
|
||||
}
|
||||
gotChannels[peer.ChannelID] = true
|
||||
}
|
||||
if !gotChannels[linked.Channel.ID] || !gotChannels[publicPreview.Channel.ID] || gotChannels[outside.Channel.ID] {
|
||||
t.Fatalf("community search channels = %+v", gotChannels)
|
||||
}
|
||||
|
||||
emptyReq := &tg.MessagesSearchGlobalRequest{
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
OffsetPeer: &tg.InputPeerEmpty{},
|
||||
Limit: 20,
|
||||
}
|
||||
emptyReq.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash})
|
||||
emptyResult, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), emptyReq)
|
||||
if err != nil {
|
||||
t.Fatalf("empty community-scoped messages.searchGlobal: %v", err)
|
||||
}
|
||||
emptyResponse, ok := emptyResult.(*tg.MessagesMessages)
|
||||
if !ok || len(emptyResponse.Messages) != 0 || len(emptyResponse.Chats) != 1 {
|
||||
t.Fatalf("empty community search result = %#v, want empty messages with validated Community chat", emptyResult)
|
||||
}
|
||||
if got, ok := emptyResponse.Chats[0].(*tg.Community); !ok || got.ID != community.Community.ID {
|
||||
t.Fatalf("empty community search chat = %#v, want Community %d", emptyResponse.Chats[0], community.Community.ID)
|
||||
}
|
||||
|
||||
badHashReq := &tg.MessagesSearchGlobalRequest{
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
OffsetPeer: &tg.InputPeerEmpty{},
|
||||
Limit: 20,
|
||||
}
|
||||
badHashReq.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash + 1})
|
||||
if _, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), badHashReq); err == nil || !tgerr.Is(err, "CHANNEL_PRIVATE") {
|
||||
t.Fatalf("empty community search wrong access hash err = %v, want CHANNEL_PRIVATE", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter.Hash != 0 {
|
||||
if filter.Hash != 0 && r.deps.Communities == nil {
|
||||
hashCheck, err := r.deps.Dialogs.GetDialogsHash(ctx, userID, filter)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
|
|
@ -415,6 +415,10 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
list, err = r.withCommunityDialogList(ctx, userID, filter, list)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
if ClientTypeFrom(ctx) == ClientTypeTDesktop && tdesktop.ShouldMergePinnedIntoInitialDialogs(filter) {
|
||||
pinned, err := r.pinnedDialogsList(ctx, userID, domain.DialogMainFolderID)
|
||||
if err != nil {
|
||||
|
|
@ -422,7 +426,7 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
|||
}
|
||||
list = tdesktop.MergeInitialDialogsWithPinned(list, pinned)
|
||||
}
|
||||
if filter.Hash != 0 && list.Hash == filter.Hash {
|
||||
if filter.Hash != 0 && r.deps.Communities == nil && list.Hash == filter.Hash {
|
||||
return &tg.MessagesDialogsNotModified{Count: list.Count}, nil
|
||||
}
|
||||
return r.tgMessagesDialogs(ctx, userID, r.withDialogListPresence(ctx, userID, list)), nil
|
||||
|
|
@ -481,14 +485,31 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
|||
if err := r.checkCatchupRateLimit(ctx, userID, peerDialogsRateLimitKeyPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regularPeers := make([]domain.Peer, 0, len(domainPeers))
|
||||
communityIDs := make([]int64, 0)
|
||||
for _, peer := range domainPeers {
|
||||
if peer.Type == domain.PeerTypeCommunity {
|
||||
communityIDs = append(communityIDs, peer.ID)
|
||||
} else {
|
||||
regularPeers = append(regularPeers, peer)
|
||||
}
|
||||
}
|
||||
var list domain.DialogList
|
||||
if len(domainPeers) > 0 && r.deps.Dialogs != nil {
|
||||
if len(regularPeers) > 0 && r.deps.Dialogs != nil {
|
||||
var err error
|
||||
list, err = r.deps.Dialogs.GetPeerDialogs(ctx, userID, domainPeers)
|
||||
list, err = r.deps.Dialogs.GetPeerDialogs(ctx, userID, regularPeers)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
}
|
||||
if len(communityIDs) > 0 && r.deps.Communities != nil {
|
||||
views, err := r.deps.Communities.GetMany(ctx, userID, communityIDs)
|
||||
if err != nil {
|
||||
return nil, communityErr(err)
|
||||
}
|
||||
list.Communities = append(list.Communities, views...)
|
||||
list.Count += len(views)
|
||||
}
|
||||
st := domain.UpdateState{Date: int(r.clock.Now().Unix())}
|
||||
if r.deps.Updates != nil {
|
||||
var err error
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
|||
r.registerMessages(d)
|
||||
r.registerStickers(d)
|
||||
r.registerChannels(d)
|
||||
r.registerCommunities(d)
|
||||
r.registerUpload(d)
|
||||
r.registerPhotos(d)
|
||||
r.registerFolders(d)
|
||||
|
|
|
|||
|
|
@ -1067,21 +1067,36 @@ func (r *Router) onStoriesGetPeerMaxIDs(ctx context.Context, id []tg.InputPeerCl
|
|||
return nil, internalErr()
|
||||
}
|
||||
peers := make([]domain.Peer, 0, len(id))
|
||||
for _, input := range id {
|
||||
positions := make([]int, 0, len(id))
|
||||
result := make([]tg.RecentStory, len(id))
|
||||
for i, input := range id {
|
||||
if _, community, err := r.maybeCommunityFromInputPeer(ctx, userID, input); err != nil {
|
||||
return nil, err
|
||||
} else if community {
|
||||
// Communities are projected as channel peers in dialog lists, but do
|
||||
// not own stories. Keep the batch positional by returning an empty
|
||||
// recentStory at this index and resolve every ordinary peer normally.
|
||||
continue
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peers = append(peers, peer)
|
||||
positions = append(positions, i)
|
||||
}
|
||||
if r.deps.Stories == nil || userID == 0 {
|
||||
return make([]tg.RecentStory, len(id)), nil
|
||||
return result, nil
|
||||
}
|
||||
recent, err := r.deps.Stories.GetPeerMaxIDs(ctx, userID, peers, int(r.clock.Now().Unix()))
|
||||
if err != nil {
|
||||
return nil, storyErr(err)
|
||||
}
|
||||
return tgRecentStories(alignStoryRecentByPeer(peers, recent)), nil
|
||||
aligned := tgRecentStories(alignStoryRecentByPeer(peers, recent))
|
||||
for i, position := range positions {
|
||||
result[position] = aligned[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func alignStoryRecentByPeer(peers []domain.Peer, recent []domain.RecentStory) []domain.RecentStory {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue