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
|
|
@ -508,6 +508,15 @@ func (s *Service) ListAdminedPublicChannels(ctx context.Context, userID int64) (
|
|||
return s.channels.ListAdminedPublicChannels(ctx, userID)
|
||||
}
|
||||
|
||||
// ListCommunityLinkableChannels returns owned/administered channels that are not
|
||||
// already linked to another Community. Private megagroups are valid candidates.
|
||||
func (s *Service) ListCommunityLinkableChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
|
||||
if s == nil || s.channels == nil || userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.channels.ListCommunityLinkableChannels(ctx, userID)
|
||||
}
|
||||
|
||||
// ListStoryPostableChannels returns channels where user can publish stories.
|
||||
func (s *Service) ListStoryPostableChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
|
||||
if s == nil || s.channels == nil || userID == 0 {
|
||||
|
|
|
|||
185
internal/app/communities/service.go
Normal file
185
internal/app/communities/service.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package communities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// Service owns Community business validation. The store is the aggregate
|
||||
// transaction boundary because link changes span community/link and peer rows.
|
||||
type Service struct {
|
||||
communities store.CommunityStore
|
||||
}
|
||||
|
||||
func NewService(communities store.CommunityStore) *Service {
|
||||
return &Service{communities: communities}
|
||||
}
|
||||
|
||||
func validPeer(peer domain.Peer) bool {
|
||||
return peer.ID > 0 && (peer.Type == domain.PeerTypeChannel || peer.Type == domain.PeerTypeUser)
|
||||
}
|
||||
|
||||
func validVisibility(v domain.CommunityPeerVisibility) bool {
|
||||
return v == domain.CommunityPeerVisible || v == domain.CommunityPeerHidden
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, userID int64, req domain.CreateCommunityRequest) (domain.CommunityView, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || !validPeer(req.InitialPeer) || !validVisibility(req.Visibility) {
|
||||
return domain.CommunityView{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
req.CreatorUserID = userID
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.About = strings.TrimSpace(req.About)
|
||||
if req.Title == "" || utf8.RuneCountInString(req.Title) > domain.MaxCommunityTitleRunes {
|
||||
return domain.CommunityView{}, domain.ErrChannelTitleInvalid
|
||||
}
|
||||
if utf8.RuneCountInString(req.About) > domain.MaxCommunityAboutRunes {
|
||||
return domain.CommunityView{}, domain.ErrAboutTooLong
|
||||
}
|
||||
return s.communities.CreateCommunity(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, userID, communityID int64) (domain.CommunityView, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 {
|
||||
return domain.CommunityView{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.GetCommunity(ctx, userID, communityID)
|
||||
}
|
||||
|
||||
func (s *Service) GetMany(ctx context.Context, userID int64, ids []int64) ([]domain.CommunityView, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 {
|
||||
return nil, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.GetCommunities(ctx, userID, ids)
|
||||
}
|
||||
|
||||
func (s *Service) ListJoined(ctx context.Context, userID int64) ([]domain.CommunityView, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 {
|
||||
return nil, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.ListJoinedCommunities(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Service) TogglePeerLink(ctx context.Context, userID int64, req domain.CommunityTogglePeerLinkRequest) (domain.CommunityTogglePeerLinkResult, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || req.CommunityID == 0 || !validPeer(req.Peer) {
|
||||
return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
if !req.Deleted && !validVisibility(req.Visibility) {
|
||||
return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityPeerInvalid
|
||||
}
|
||||
req.ActorUserID = userID
|
||||
return s.communities.ToggleCommunityPeerLink(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) SetCollapsed(ctx context.Context, userID, communityID int64, collapsed bool) (domain.CommunityView, bool, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 {
|
||||
return domain.CommunityView{}, false, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.SetCommunityCollapsed(ctx, userID, communityID, collapsed)
|
||||
}
|
||||
|
||||
func (s *Service) ListPeerLinkRequests(ctx context.Context, userID, communityID int64, offset string, limit int) (domain.CommunityPeerLinkRequestPage, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 {
|
||||
return domain.CommunityPeerLinkRequestPage{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxCommunityLinkRequests {
|
||||
limit = domain.MaxCommunityLinkRequests
|
||||
}
|
||||
return s.communities.ListCommunityPeerLinkRequests(ctx, userID, communityID, offset, limit)
|
||||
}
|
||||
|
||||
func (s *Service) DecidePeerLinkRequest(ctx context.Context, userID, communityID int64, peer domain.Peer, reject bool, date int) (domain.CommunityTogglePeerLinkResult, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 || !validPeer(peer) {
|
||||
return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.DecideCommunityPeerLinkRequest(ctx, userID, communityID, peer, reject, date)
|
||||
}
|
||||
|
||||
func (s *Service) DecideAllPeerLinkRequests(ctx context.Context, userID, communityID int64, reject bool, date int) ([]domain.CommunityTogglePeerLinkResult, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 {
|
||||
return nil, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.DecideAllCommunityPeerLinkRequests(ctx, userID, communityID, reject, date)
|
||||
}
|
||||
|
||||
func (s *Service) ToggleParticipantBanned(ctx context.Context, userID, communityID, participantUserID int64, unban bool, date int) (domain.CommunityParticipantBanResult, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 || participantUserID == 0 {
|
||||
return domain.CommunityParticipantBanResult{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.ToggleCommunityParticipantBanned(ctx, userID, communityID, participantUserID, unban, date)
|
||||
}
|
||||
|
||||
func (s *Service) ParticipantJoinedChats(ctx context.Context, userID, communityID, participantUserID int64) (domain.CommunityParticipantJoinedChats, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 || participantUserID == 0 {
|
||||
return domain.CommunityParticipantJoinedChats{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
return s.communities.GetCommunityParticipantJoinedChats(ctx, userID, communityID, participantUserID)
|
||||
}
|
||||
|
||||
func (s *Service) Participants(ctx context.Context, userID, communityID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.CommunityParticipantList, error) {
|
||||
if s == nil || s.communities == nil || userID == 0 || communityID == 0 {
|
||||
return domain.CommunityParticipantList{}, domain.ErrCommunityInvalid
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > domain.MaxChannelParticipantsOffset {
|
||||
offset = domain.MaxChannelParticipantsOffset
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxCommunityParticipants {
|
||||
limit = domain.MaxCommunityParticipants
|
||||
}
|
||||
return s.communities.ListCommunityParticipants(ctx, userID, communityID, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (s *Service) EditTitle(ctx context.Context, userID, communityID int64, title string) (domain.CommunityView, bool, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" || utf8.RuneCountInString(title) > domain.MaxCommunityTitleRunes {
|
||||
return domain.CommunityView{}, false, domain.ErrChannelTitleInvalid
|
||||
}
|
||||
return s.communities.EditCommunityTitle(ctx, userID, communityID, title)
|
||||
}
|
||||
|
||||
func (s *Service) EditAbout(ctx context.Context, userID, communityID int64, about string) (domain.CommunityView, bool, error) {
|
||||
about = strings.TrimSpace(about)
|
||||
if utf8.RuneCountInString(about) > domain.MaxCommunityAboutRunes {
|
||||
return domain.CommunityView{}, false, domain.ErrAboutTooLong
|
||||
}
|
||||
return s.communities.EditCommunityAbout(ctx, userID, communityID, about)
|
||||
}
|
||||
|
||||
func (s *Service) EditAdmin(ctx context.Context, userID int64, req domain.CommunityEditAdminRequest) (domain.CommunityView, bool, error) {
|
||||
if req.CommunityID == 0 || req.UserID == 0 || userID == 0 {
|
||||
return domain.CommunityView{}, false, domain.ErrCommunityInvalid
|
||||
}
|
||||
req.ActorUserID = userID
|
||||
return s.communities.EditCommunityAdmin(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) EditDefaultBannedRights(ctx context.Context, userID, communityID int64, rights domain.ChannelBannedRights) (domain.CommunityView, bool, error) {
|
||||
return s.communities.EditCommunityDefaultBannedRights(ctx, userID, communityID, rights)
|
||||
}
|
||||
|
||||
func (s *Service) SetPhoto(ctx context.Context, userID, communityID int64, photo *domain.Photo, date int) (domain.CommunityView, bool, error) {
|
||||
return s.communities.SetCommunityPhoto(ctx, userID, communityID, photo, date)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, userID, communityID int64, date int) (domain.CommunityView, []domain.Peer, error) {
|
||||
return s.communities.DeleteCommunity(ctx, userID, communityID, date)
|
||||
}
|
||||
|
||||
func (s *Service) SetPinned(ctx context.Context, userID, communityID int64, pinned bool) (bool, error) {
|
||||
return s.communities.SetCommunityPinned(ctx, userID, communityID, pinned)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderPinned(ctx context.Context, userID int64, order []domain.Peer, force bool) (bool, error) {
|
||||
return s.communities.ReorderCommunityPinned(ctx, userID, order, force)
|
||||
}
|
||||
|
||||
func (s *Service) SearchScope(ctx context.Context, userID, communityID int64) (domain.CommunitySearchScope, error) {
|
||||
return s.communities.CommunitySearchScope(ctx, userID, communityID)
|
||||
}
|
||||
|
|
@ -280,7 +280,10 @@ type ChannelBannedRights struct {
|
|||
SendPlain bool
|
||||
EditRank bool
|
||||
SendReactions bool
|
||||
UntilDate int
|
||||
// ManageLinkedPeers is the Layer 228 default restriction used by Communities:
|
||||
// true means only admins may add peers; false lets members submit requests.
|
||||
ManageLinkedPeers bool
|
||||
UntilDate int
|
||||
}
|
||||
|
||||
// ChannelReactionPolicyType describes which reactions are allowed in a channel.
|
||||
|
|
@ -431,6 +434,10 @@ type Channel struct {
|
|||
// megagroup && (public || has_geo || has_link) 判定是否拉取候选列表。
|
||||
HasLink bool
|
||||
LinkedChatID int64
|
||||
// LinkedCommunityID is the unique Community containing this group/channel.
|
||||
// A channel can belong to at most one Community and Communities themselves
|
||||
// are stored in a separate aggregate, never in channels.
|
||||
LinkedCommunityID int64
|
||||
// Monoforum 标记本频道是「频道私信(Direct Messages)」的 monoforum 虚拟频道。
|
||||
// LinkedMonoforumID:母频道指向其 monoforum;monoforum 反向指向母频道(双向)。
|
||||
Monoforum bool
|
||||
|
|
@ -561,11 +568,15 @@ const (
|
|||
ChannelActionStarGiftUnique ChannelMessageActionType = "star_gift_unique"
|
||||
// ChannelActionSetChatWallpaper 映射 messageActionSetChatWallPaper:频道外观页设置 wallpaper。
|
||||
ChannelActionSetChatWallpaper ChannelMessageActionType = "set_chat_wallpaper"
|
||||
// ChannelActionChangeCommunity maps messageActionChangeCommunity. A non-zero
|
||||
// CommunityID means linked; zero means unlinked.
|
||||
ChannelActionChangeCommunity ChannelMessageActionType = "change_community"
|
||||
)
|
||||
|
||||
// ChannelMessageAction describes a service action without depending on tg.*.
|
||||
type ChannelMessageAction struct {
|
||||
Type ChannelMessageActionType
|
||||
CommunityID int64
|
||||
Title string
|
||||
IconColor int
|
||||
IconEmojiID int64
|
||||
|
|
@ -2039,18 +2050,24 @@ type ChannelSearchPostsRequest struct {
|
|||
// ChannelGlobalSearchRequest describes a bounded messages.searchGlobal page
|
||||
// over channel/supergroup messages visible to the current account.
|
||||
type ChannelGlobalSearchRequest struct {
|
||||
Query string
|
||||
BroadcastsOnly bool
|
||||
GroupsOnly bool
|
||||
MusicOnly bool
|
||||
HasFolderID bool
|
||||
FolderID int
|
||||
OffsetRate int
|
||||
OffsetChannelID int64
|
||||
OffsetID int
|
||||
MinDate int
|
||||
MaxDate int
|
||||
Limit int
|
||||
Query string
|
||||
ChannelIDs []int64
|
||||
RestrictChannelIDs bool
|
||||
// AllowPublicPreview includes linked public channels that the account can
|
||||
// preview without joining. It is enabled only by Layer 228 Community-scoped
|
||||
// search; ordinary global search remains joined-dialog-only.
|
||||
AllowPublicPreview bool
|
||||
BroadcastsOnly bool
|
||||
GroupsOnly bool
|
||||
MusicOnly bool
|
||||
HasFolderID bool
|
||||
FolderID int
|
||||
OffsetRate int
|
||||
OffsetChannelID int64
|
||||
OffsetID int
|
||||
MinDate int
|
||||
MaxDate int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// ChannelRepliesFilter describes messages.getReplies query conditions.
|
||||
|
|
|
|||
214
internal/domain/community.go
Normal file
214
internal/domain/community.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
const (
|
||||
MaxCommunityPeers = 100
|
||||
MaxCommunityBotPeers = 100
|
||||
MaxCommunityLinkRequests = 100
|
||||
MaxCommunityTitleRunes = 128
|
||||
MaxCommunityAboutRunes = 255
|
||||
MaxCommunityParticipants = 200
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommunityInvalid = errors.New("community invalid")
|
||||
ErrCommunityPrivate = errors.New("community private")
|
||||
ErrCommunityAdminRequired = errors.New("community admin required")
|
||||
ErrCommunityCreatorRequired = errors.New("community creator required")
|
||||
ErrCommunityPeerInvalid = errors.New("community peer invalid")
|
||||
ErrCommunityPeerLinked = errors.New("community peer already linked")
|
||||
ErrCommunityPeersTooMuch = errors.New("community peers too much")
|
||||
ErrCommunityRequestCreated = errors.New("community request created")
|
||||
ErrCommunityRequestMissing = errors.New("community request missing")
|
||||
ErrCommunityParticipantInvalid = errors.New("community participant invalid")
|
||||
)
|
||||
|
||||
// Community is the Layer 228 aggregation container. It intentionally has no
|
||||
// message/read/pts fields: linked dialogs remain the only message truth.
|
||||
type Community struct {
|
||||
ID int64
|
||||
AccessHash int64
|
||||
CreatorUserID int64
|
||||
Title string
|
||||
About string
|
||||
Date int
|
||||
Deleted bool
|
||||
DefaultBannedRights ChannelBannedRights
|
||||
PhotoID int64
|
||||
PhotoDCID int
|
||||
PhotoStripped []byte
|
||||
}
|
||||
|
||||
type CommunityMemberRole string
|
||||
|
||||
const (
|
||||
CommunityRoleCreator CommunityMemberRole = "creator"
|
||||
CommunityRoleAdmin CommunityMemberRole = "admin"
|
||||
CommunityRoleMember CommunityMemberRole = "member"
|
||||
)
|
||||
|
||||
type CommunityMemberStatus string
|
||||
|
||||
const (
|
||||
CommunityMemberActive CommunityMemberStatus = "active"
|
||||
CommunityMemberKicked CommunityMemberStatus = "kicked"
|
||||
)
|
||||
|
||||
type CommunityMember struct {
|
||||
CommunityID int64
|
||||
UserID int64
|
||||
Role CommunityMemberRole
|
||||
Status CommunityMemberStatus
|
||||
AdminRights ChannelAdminRights
|
||||
Rank string
|
||||
Date int
|
||||
}
|
||||
|
||||
func (m CommunityMember) Active() bool { return m.Status == CommunityMemberActive }
|
||||
|
||||
func (m CommunityMember) CanManageLinkedPeers() bool {
|
||||
return m.Active() && (m.Role == CommunityRoleCreator ||
|
||||
(m.Role == CommunityRoleAdmin && m.AdminRights.ManageLinkedPeers))
|
||||
}
|
||||
|
||||
func (m CommunityMember) CanChangeInfo() bool {
|
||||
return m.Active() && (m.Role == CommunityRoleCreator ||
|
||||
(m.Role == CommunityRoleAdmin && m.AdminRights.ChangeInfo))
|
||||
}
|
||||
|
||||
func (m CommunityMember) CanAddAdmins() bool {
|
||||
return m.Active() && (m.Role == CommunityRoleCreator ||
|
||||
(m.Role == CommunityRoleAdmin && m.AdminRights.AddAdmins))
|
||||
}
|
||||
|
||||
func (m CommunityMember) CanBanUsers() bool {
|
||||
return m.Active() && (m.Role == CommunityRoleCreator ||
|
||||
(m.Role == CommunityRoleAdmin && m.AdminRights.BanUsers))
|
||||
}
|
||||
|
||||
type CommunityPeerVisibility string
|
||||
|
||||
const (
|
||||
CommunityPeerVisible CommunityPeerVisibility = "visible"
|
||||
CommunityPeerHidden CommunityPeerVisibility = "hidden"
|
||||
)
|
||||
|
||||
type CommunityPeerLink struct {
|
||||
CommunityID int64
|
||||
Peer Peer
|
||||
Visibility CommunityPeerVisibility
|
||||
CanViewHistory bool
|
||||
CreatedBy int64
|
||||
Date int
|
||||
}
|
||||
|
||||
func (l CommunityPeerLink) Visible() bool { return l.Visibility == CommunityPeerVisible }
|
||||
|
||||
type CommunityPeerLinkRequest struct {
|
||||
CommunityID int64
|
||||
Peer Peer
|
||||
RequestedBy int64
|
||||
Visibility CommunityPeerVisibility
|
||||
Date int
|
||||
}
|
||||
|
||||
type CommunityUserState struct {
|
||||
CommunityID int64
|
||||
UserID int64
|
||||
Collapsed bool
|
||||
Pinned bool
|
||||
PinnedOrder int
|
||||
NotifySettings *PeerNotifySettings
|
||||
}
|
||||
|
||||
type CommunityView struct {
|
||||
Community Community
|
||||
Self CommunityMember
|
||||
State CommunityUserState
|
||||
Links []CommunityPeerLink
|
||||
Channels []Channel
|
||||
Users []User
|
||||
ServiceMessages []SendChannelMessageResult
|
||||
AdminsCount int
|
||||
KickedCount int
|
||||
PendingRequests int
|
||||
Forbidden bool
|
||||
}
|
||||
|
||||
func (v CommunityView) Creator() bool {
|
||||
return v.Self.Active() && v.Self.Role == CommunityRoleCreator
|
||||
}
|
||||
|
||||
type CreateCommunityRequest struct {
|
||||
CreatorUserID int64
|
||||
Title string
|
||||
About string
|
||||
InitialPeer Peer
|
||||
Visibility CommunityPeerVisibility
|
||||
Date int
|
||||
}
|
||||
|
||||
type CommunityTogglePeerLinkRequest struct {
|
||||
ActorUserID int64
|
||||
CommunityID int64
|
||||
Peer Peer
|
||||
Visibility CommunityPeerVisibility
|
||||
Deleted bool
|
||||
RequestOnly bool
|
||||
Date int
|
||||
}
|
||||
|
||||
type CommunityTogglePeerLinkResult struct {
|
||||
Community Community
|
||||
Peer Peer
|
||||
RequestedBy int64
|
||||
Link *CommunityPeerLink
|
||||
ServiceMessage *SendChannelMessageResult
|
||||
Removed bool
|
||||
RequestCreated bool
|
||||
}
|
||||
|
||||
type CommunityPeerLinkRequestPage struct {
|
||||
TotalCount int
|
||||
Requests []CommunityPeerLinkRequest
|
||||
NextOffset string
|
||||
Channels []Channel
|
||||
Users []User
|
||||
}
|
||||
|
||||
type CommunityParticipantJoinedChats struct {
|
||||
CreatorChatIDs []int64
|
||||
JoinedChatIDs []int64
|
||||
Channels []Channel
|
||||
Users []User
|
||||
}
|
||||
|
||||
type CommunityParticipantList struct {
|
||||
Community Community
|
||||
Count int
|
||||
Participants []CommunityMember
|
||||
Users []User
|
||||
Hash int64
|
||||
}
|
||||
|
||||
type CommunityParticipantBanResult struct {
|
||||
Changed bool
|
||||
ChannelBans []EditChannelBannedResult
|
||||
RemovedLinks []CommunityTogglePeerLinkResult
|
||||
}
|
||||
|
||||
type CommunityEditAdminRequest struct {
|
||||
ActorUserID int64
|
||||
CommunityID int64
|
||||
UserID int64
|
||||
Rights ChannelAdminRights
|
||||
Rank string
|
||||
Date int
|
||||
}
|
||||
|
||||
type CommunitySearchScope struct {
|
||||
CommunityID int64
|
||||
ChannelIDs []int64
|
||||
BotUserIDs []int64
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ type PeerType string
|
|||
const (
|
||||
PeerTypeUser PeerType = "user"
|
||||
PeerTypeChannel PeerType = "channel"
|
||||
// PeerTypeCommunity identifies a Layer 228 Community container. Communities
|
||||
// have dialog pin/notify state but never own messages, read boundaries or pts.
|
||||
PeerTypeCommunity PeerType = "community"
|
||||
// PeerTypeFolder 仅用于 dialog 置顶事件中表达 dialogPeerFolder
|
||||
// (archive folder 行本身被置顶/取消置顶),ID 为 folder_id。
|
||||
PeerTypeFolder PeerType = "folder"
|
||||
|
|
@ -175,6 +178,7 @@ type DialogList struct {
|
|||
ChannelMessages []ChannelMessage
|
||||
Users []User
|
||||
Channels []Channel
|
||||
Communities []CommunityView
|
||||
State UpdateState
|
||||
Hash int64
|
||||
Count int
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@ type MessageFilter struct {
|
|||
// SavedPeer 非零时仅返回 self-chat 中该 saved 子会话的消息
|
||||
// (messages.getSavedHistory);Peer 必须同时是 self。
|
||||
SavedPeer Peer
|
||||
// PeerIDs restricts a global private search to these user peers. Empty is a
|
||||
// valid restricted set, so RestrictPeerIDs carries presence separately.
|
||||
PeerIDs []int64
|
||||
RestrictPeerIDs bool
|
||||
}
|
||||
|
||||
// SendPrivateTextRequest 是私聊文本/媒体发送命令。
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ type User struct {
|
|||
// PersonalChannelID 是资料页展示的「个人频道」(account.updatePersonalChannel);
|
||||
// 0 表示未设置。资料投影时按它取频道对象与最新一帖。
|
||||
PersonalChannelID int64
|
||||
// LinkedCommunityID is the single Community containing this bot. Ordinary
|
||||
// users must keep it zero; the community aggregate enforces that invariant.
|
||||
LinkedCommunityID int64
|
||||
Color PeerColor
|
||||
ProfileColor PeerColor
|
||||
// Profile photo fields are filled by app-layer user projection. PhotoID==0 表示无头像。
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type ChannelStore interface {
|
|||
UpdateUsername(ctx context.Context, req domain.UpdateChannelUsernameRequest) (domain.Channel, error)
|
||||
SetChannelVerified(ctx context.Context, channelID int64, verified bool) (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)
|
||||
// ResolvePublicChannelUsername resolves an active public channel/supergroup.
|
||||
|
|
|
|||
34
internal/store/community.go
Normal file
34
internal/store/community.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// CommunityStore persists the Layer 228 Community aggregate. Mutations that
|
||||
// touch a link and the linked peer's denormalized linked_community_id must be
|
||||
// atomic in durable implementations.
|
||||
type CommunityStore interface {
|
||||
CreateCommunity(ctx context.Context, req domain.CreateCommunityRequest) (domain.CommunityView, error)
|
||||
GetCommunity(ctx context.Context, viewerUserID, communityID int64) (domain.CommunityView, error)
|
||||
GetCommunities(ctx context.Context, viewerUserID int64, communityIDs []int64) ([]domain.CommunityView, error)
|
||||
ListJoinedCommunities(ctx context.Context, viewerUserID int64) ([]domain.CommunityView, error)
|
||||
ToggleCommunityPeerLink(ctx context.Context, req domain.CommunityTogglePeerLinkRequest) (domain.CommunityTogglePeerLinkResult, error)
|
||||
SetCommunityCollapsed(ctx context.Context, userID, communityID int64, collapsed bool) (domain.CommunityView, bool, error)
|
||||
ListCommunityPeerLinkRequests(ctx context.Context, viewerUserID, communityID int64, offset string, limit int) (domain.CommunityPeerLinkRequestPage, error)
|
||||
DecideCommunityPeerLinkRequest(ctx context.Context, actorUserID, communityID int64, peer domain.Peer, reject bool, date int) (domain.CommunityTogglePeerLinkResult, error)
|
||||
DecideAllCommunityPeerLinkRequests(ctx context.Context, actorUserID, communityID int64, reject bool, date int) ([]domain.CommunityTogglePeerLinkResult, error)
|
||||
ToggleCommunityParticipantBanned(ctx context.Context, actorUserID, communityID, participantUserID int64, unban bool, date int) (domain.CommunityParticipantBanResult, error)
|
||||
GetCommunityParticipantJoinedChats(ctx context.Context, viewerUserID, communityID, participantUserID int64) (domain.CommunityParticipantJoinedChats, error)
|
||||
ListCommunityParticipants(ctx context.Context, viewerUserID, communityID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.CommunityParticipantList, error)
|
||||
EditCommunityTitle(ctx context.Context, actorUserID, communityID int64, title string) (domain.CommunityView, bool, error)
|
||||
EditCommunityAbout(ctx context.Context, actorUserID, communityID int64, about string) (domain.CommunityView, bool, error)
|
||||
EditCommunityAdmin(ctx context.Context, req domain.CommunityEditAdminRequest) (domain.CommunityView, bool, error)
|
||||
EditCommunityDefaultBannedRights(ctx context.Context, actorUserID, communityID int64, rights domain.ChannelBannedRights) (domain.CommunityView, bool, error)
|
||||
SetCommunityPhoto(ctx context.Context, actorUserID, communityID int64, photo *domain.Photo, date int) (domain.CommunityView, bool, error)
|
||||
DeleteCommunity(ctx context.Context, actorUserID, communityID int64, date int) (domain.CommunityView, []domain.Peer, error)
|
||||
SetCommunityPinned(ctx context.Context, userID, communityID int64, pinned bool) (changed bool, err error)
|
||||
ReorderCommunityPinned(ctx context.Context, userID int64, order []domain.Peer, force bool) (changed bool, err error)
|
||||
CommunitySearchScope(ctx context.Context, viewerUserID, communityID int64) (domain.CommunitySearchScope, error)
|
||||
}
|
||||
|
|
@ -655,6 +655,31 @@ func (s *ChannelStore) ListAdminedPublicChannels(_ context.Context, userID int64
|
|||
return append([]domain.Channel(nil), out...), nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListCommunityLinkableChannels(_ context.Context, userID int64) ([]domain.Channel, error) {
|
||||
if userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]domain.Channel, 0)
|
||||
for channelID, members := range s.members {
|
||||
member := members[userID]
|
||||
if member.Status != domain.ChannelMemberActive || !isChannelAdmin(member) {
|
||||
continue
|
||||
}
|
||||
channel, ok := s.channels[channelID]
|
||||
if !ok || channel.Deleted || channel.Monoforum || channel.LinkedCommunityID != 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, channel)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
||||
if len(out) > domain.MaxCommunityPeers {
|
||||
out = out[:domain.MaxCommunityPeers]
|
||||
}
|
||||
return append([]domain.Channel(nil), out...), nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListStoryPostableChannels(_ context.Context, userID int64) ([]domain.Channel, error) {
|
||||
if userID == 0 {
|
||||
return nil, nil
|
||||
|
|
|
|||
|
|
@ -192,7 +192,16 @@ func (s *ChannelStore) SearchJoinedMessages(_ context.Context, viewerUserID int6
|
|||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
hits := make([]hit, 0, req.Limit+1)
|
||||
channelIDs := make(map[int64]struct{}, len(req.ChannelIDs))
|
||||
for _, id := range req.ChannelIDs {
|
||||
channelIDs[id] = struct{}{}
|
||||
}
|
||||
for channelID, channel := range s.channels {
|
||||
if req.RestrictChannelIDs {
|
||||
if _, ok := channelIDs[channelID]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if channel.Deleted {
|
||||
continue
|
||||
}
|
||||
|
|
@ -203,7 +212,10 @@ func (s *ChannelStore) SearchJoinedMessages(_ context.Context, viewerUserID int6
|
|||
continue
|
||||
}
|
||||
member, ok := s.members[channelID][viewerUserID]
|
||||
if !ok || member.Status != domain.ChannelMemberActive || member.BannedRights.ViewMessages {
|
||||
joined := ok && member.Status == domain.ChannelMemberActive && !member.BannedRights.ViewMessages
|
||||
publicPreview := req.AllowPublicPreview && publicPreviewableChannel(channel) &&
|
||||
(!ok || member.Status != domain.ChannelMemberKicked && !member.BannedRights.ViewMessages)
|
||||
if !joined && !publicPreview {
|
||||
continue
|
||||
}
|
||||
if req.HasFolderID {
|
||||
|
|
@ -219,7 +231,7 @@ func (s *ChannelStore) SearchJoinedMessages(_ context.Context, viewerUserID int6
|
|||
if query == "" && !req.MusicOnly || query != "" && strings.TrimSpace(msg.Body) == "" {
|
||||
continue
|
||||
}
|
||||
if member.AvailableMinID > 0 && msg.ID <= member.AvailableMinID {
|
||||
if joined && member.AvailableMinID > 0 && msg.ID <= member.AvailableMinID {
|
||||
continue
|
||||
}
|
||||
if req.MinDate > 0 && msg.Date <= req.MinDate {
|
||||
|
|
|
|||
1112
internal/store/memory/community.go
Normal file
1112
internal/store/memory/community.go
Normal file
File diff suppressed because it is too large
Load diff
285
internal/store/memory/community_test.go
Normal file
285
internal/store/memory/community_test.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func mustCommunityTestUser(t *testing.T, store *UserStore, firstName, phone string) domain.User {
|
||||
t.Helper()
|
||||
user, err := store.Create(context.Background(), domain.User{AccessHash: int64(len(phone) + len(firstName)), Phone: phone, FirstName: firstName})
|
||||
if err != nil {
|
||||
t.Fatalf("create user %q: %v", firstName, err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func mustCommunityTestChannel(t *testing.T, store *ChannelStore, 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 := store.CreateChannel(context.Background(), domain.CreateChannelRequest{
|
||||
CreatorUserID: creator.ID,
|
||||
Title: title,
|
||||
Megagroup: true,
|
||||
MemberUserIDs: memberIDs,
|
||||
Date: 1_800_000_000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel %q: %v", title, err)
|
||||
}
|
||||
return created.Channel
|
||||
}
|
||||
|
||||
func TestCommunityLifecycleRequestsSearchAndModeration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := NewUserStore()
|
||||
owner := mustCommunityTestUser(t, users, "Community Owner", "15551000001")
|
||||
member := mustCommunityTestUser(t, users, "Alice Searchable", "15551000002")
|
||||
channels := NewChannelStore()
|
||||
initial := mustCommunityTestChannel(t, channels, owner, "Initial", member)
|
||||
store := NewCommunityStore(users, channels, nil, nil)
|
||||
|
||||
created, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Engineering",
|
||||
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: initial.ID},
|
||||
Visibility: domain.CommunityPeerHidden,
|
||||
Date: 1_800_000_001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create community: %v", err)
|
||||
}
|
||||
if len(created.Links) != 1 || created.Links[0].Visibility != domain.CommunityPeerHidden {
|
||||
t.Fatalf("initial links = %+v, want one hidden link", created.Links)
|
||||
}
|
||||
if len(created.ServiceMessages) != 1 || created.ServiceMessages[0].Message.Action == nil ||
|
||||
created.ServiceMessages[0].Message.Action.Type != domain.ChannelActionChangeCommunity ||
|
||||
created.ServiceMessages[0].Message.Action.CommunityID != created.Community.ID || created.ServiceMessages[0].Event.Pts == 0 {
|
||||
t.Fatalf("create service messages = %+v, want durable change-community action", created.ServiceMessages)
|
||||
}
|
||||
initialView, err := channels.GetChannel(ctx, owner.ID, initial.ID)
|
||||
if err != nil || initialView.Channel.LinkedCommunityID != created.Community.ID {
|
||||
t.Fatalf("initial linked community = %d, err=%v, want %d", initialView.Channel.LinkedCommunityID, err, created.Community.ID)
|
||||
}
|
||||
|
||||
_, err = store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Duplicate",
|
||||
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: initial.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_000_002,
|
||||
})
|
||||
if !errors.Is(err, domain.ErrCommunityPeerLinked) {
|
||||
t.Fatalf("reuse linked peer error = %v, want ErrCommunityPeerLinked", err)
|
||||
}
|
||||
|
||||
owned := mustCommunityTestChannel(t, channels, member, "Member Owned")
|
||||
requested, err := store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
||||
ActorUserID: member.ID,
|
||||
CommunityID: created.Community.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: owned.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_000_003,
|
||||
})
|
||||
if err != nil || !requested.RequestCreated {
|
||||
t.Fatalf("link request = %+v, err=%v, want pending request", requested, err)
|
||||
}
|
||||
page, err := store.ListCommunityPeerLinkRequests(ctx, owner.ID, created.Community.ID, "", 20)
|
||||
if err != nil || page.TotalCount != 1 || len(page.Requests) != 1 || page.Requests[0].RequestedBy != member.ID {
|
||||
t.Fatalf("request page = %+v, err=%v", page, err)
|
||||
}
|
||||
approved, err := store.DecideCommunityPeerLinkRequest(ctx, owner.ID, created.Community.ID, requested.Peer, false, 1_800_000_004)
|
||||
if err != nil || approved.Link == nil || approved.RequestedBy != member.ID || approved.ServiceMessage == nil {
|
||||
t.Fatalf("approved request = %+v, err=%v", approved, err)
|
||||
}
|
||||
ownerView, err := store.GetCommunity(ctx, owner.ID, created.Community.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("owner community view after approval: %v", err)
|
||||
}
|
||||
for _, link := range ownerView.Links {
|
||||
if link.Peer == approved.Peer && link.CanViewHistory {
|
||||
t.Fatalf("community admin without private-channel membership advertised can_view_history")
|
||||
}
|
||||
}
|
||||
privateVisible := mustCommunityTestChannel(t, channels, owner, "Private Visible")
|
||||
linked, err := store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
||||
ActorUserID: owner.ID,
|
||||
CommunityID: created.Community.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: privateVisible.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_000_004,
|
||||
})
|
||||
if err != nil || linked.Link == nil {
|
||||
t.Fatalf("link private visible channel = %+v, err=%v", linked, err)
|
||||
}
|
||||
memberView, err := store.GetCommunity(ctx, member.ID, created.Community.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("member community view: %v", err)
|
||||
}
|
||||
foundPrivate := false
|
||||
for _, link := range memberView.Links {
|
||||
if link.Peer.ID == privateVisible.ID {
|
||||
foundPrivate = true
|
||||
if link.CanViewHistory {
|
||||
t.Fatalf("private visible channel advertised can_view_history to non-member")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundPrivate {
|
||||
t.Fatalf("visible private channel missing from member community view")
|
||||
}
|
||||
|
||||
_, err = store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
||||
ActorUserID: owner.ID,
|
||||
CommunityID: created.Community.ID,
|
||||
Peer: approved.Peer,
|
||||
Visibility: domain.CommunityPeerHidden,
|
||||
Date: 1_800_000_005,
|
||||
})
|
||||
if !errors.Is(err, domain.ErrCommunityPeerLinked) {
|
||||
t.Fatalf("change visibility in place error = %v, want unlink/relink requirement", err)
|
||||
}
|
||||
|
||||
participants, err := store.ListCommunityParticipants(ctx, owner.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsSearch,
|
||||
Query: "searchABLE",
|
||||
}, 0, 20)
|
||||
if err != nil || participants.Count != 1 || len(participants.Participants) != 1 || participants.Participants[0].UserID != member.ID {
|
||||
t.Fatalf("participant name search = %+v, err=%v, want member", participants, err)
|
||||
}
|
||||
admins, err := store.ListCommunityParticipants(ctx, member.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsAdmins,
|
||||
}, 0, 100)
|
||||
if err != nil || admins.Count != 1 || len(admins.Participants) != 1 || admins.Participants[0].UserID != owner.ID {
|
||||
t.Fatalf("member-visible Community admins = %+v, err=%v, want creator", admins, err)
|
||||
}
|
||||
if _, err := store.ListCommunityParticipants(ctx, member.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsKicked,
|
||||
}, 0, 100); !errors.Is(err, domain.ErrCommunityAdminRequired) {
|
||||
t.Fatalf("member Community kicked list error = %v, want admin required", err)
|
||||
}
|
||||
outsider := mustCommunityTestUser(t, users, "Community Outsider", "15551000003")
|
||||
if _, err := store.ListCommunityParticipants(ctx, outsider.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsAdmins,
|
||||
}, 0, 100); !errors.Is(err, domain.ErrCommunityPrivate) {
|
||||
t.Fatalf("outsider Community admins error = %v, want private", err)
|
||||
}
|
||||
|
||||
ban, err := store.ToggleCommunityParticipantBanned(ctx, owner.ID, created.Community.ID, member.ID, false, 1_800_000_006)
|
||||
if err != nil {
|
||||
t.Fatalf("ban community participant: %v", err)
|
||||
}
|
||||
if !ban.Changed || len(ban.ChannelBans) != 1 || len(ban.RemovedLinks) != 1 || ban.RemovedLinks[0].Peer.ID != owned.ID {
|
||||
t.Fatalf("ban result = %+v, want one channel ban and owned-link removal", ban)
|
||||
}
|
||||
if action := ban.RemovedLinks[0].ServiceMessage.Message.Action; action == nil || action.Type != domain.ChannelActionChangeCommunity || action.CommunityID != 0 {
|
||||
t.Fatalf("unlink service action = %+v, want change-community(0)", action)
|
||||
}
|
||||
if got := ban.RemovedLinks[0].ServiceMessage.Channel.LinkedCommunityID; got != 0 {
|
||||
t.Fatalf("unlink service channel linked community = %d, want 0", got)
|
||||
}
|
||||
kicked, err := channels.GetParticipant(ctx, owner.ID, initial.ID, member.ID)
|
||||
if err != nil || kicked.Status != domain.ChannelMemberKicked || !kicked.BannedRights.ViewMessages {
|
||||
t.Fatalf("linked channel participant = %+v, err=%v, want kicked", kicked, err)
|
||||
}
|
||||
ownedView, err := channels.GetChannel(ctx, member.ID, owned.ID)
|
||||
if err != nil || ownedView.Channel.LinkedCommunityID != 0 {
|
||||
t.Fatalf("owned channel linked community = %d, err=%v, want 0", ownedView.Channel.LinkedCommunityID, err)
|
||||
}
|
||||
if _, err := store.GetCommunity(ctx, member.ID, created.Community.ID); !errors.Is(err, domain.ErrCommunityPrivate) {
|
||||
t.Fatalf("banned member get community error = %v, want private", err)
|
||||
}
|
||||
repeated, err := store.ToggleCommunityParticipantBanned(ctx, owner.ID, created.Community.ID, member.ID, false, 1_800_000_007)
|
||||
if err != nil || repeated.Changed || len(repeated.ChannelBans) != 0 || len(repeated.RemovedLinks) != 0 {
|
||||
t.Fatalf("repeated ban = %+v err=%v, want idempotent no-op", repeated, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommunityCollapsedPinAndMixedOrder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := NewUserStore()
|
||||
owner := mustCommunityTestUser(t, users, "Owner", "15551000011")
|
||||
channels := NewChannelStore()
|
||||
store := NewCommunityStore(users, channels, nil, nil)
|
||||
|
||||
makeCommunity := func(title string) domain.CommunityView {
|
||||
channel := mustCommunityTestChannel(t, channels, owner, title+" Channel")
|
||||
view, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: title,
|
||||
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_000_100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create %s: %v", title, err)
|
||||
}
|
||||
if _, changed, err := store.SetCommunityCollapsed(ctx, owner.ID, view.Community.ID, true); err != nil || !changed {
|
||||
t.Fatalf("collapse %s: changed=%v err=%v", title, changed, err)
|
||||
}
|
||||
if changed, err := store.SetCommunityPinned(ctx, owner.ID, view.Community.ID, true); err != nil || !changed {
|
||||
t.Fatalf("pin %s: changed=%v err=%v", title, changed, err)
|
||||
}
|
||||
return view
|
||||
}
|
||||
one := makeCommunity("One")
|
||||
two := makeCommunity("Two")
|
||||
changed, err := store.ReorderCommunityPinned(ctx, owner.ID, []domain.Peer{
|
||||
{Type: domain.PeerTypeChannel, ID: 77},
|
||||
{Type: domain.PeerTypeCommunity, ID: one.Community.ID},
|
||||
{Type: domain.PeerTypeUser, ID: 88},
|
||||
{Type: domain.PeerTypeCommunity, ID: two.Community.ID},
|
||||
}, true)
|
||||
if err != nil || !changed {
|
||||
t.Fatalf("mixed pinned reorder: changed=%v err=%v", changed, err)
|
||||
}
|
||||
oneView, _ := store.GetCommunity(ctx, owner.ID, one.Community.ID)
|
||||
twoView, _ := store.GetCommunity(ctx, owner.ID, two.Community.ID)
|
||||
if !oneView.State.Pinned || !twoView.State.Pinned || oneView.State.PinnedOrder <= twoView.State.PinnedOrder {
|
||||
t.Fatalf("pinned orders one=%+v two=%+v, want global mixed order preserved", oneView.State, twoView.State)
|
||||
}
|
||||
uncollapsed, changed, err := store.SetCommunityCollapsed(ctx, owner.ID, one.Community.ID, false)
|
||||
if err != nil || !changed || uncollapsed.State.Pinned {
|
||||
t.Fatalf("uncollapse state = %+v changed=%v err=%v, want pin cleared", uncollapsed.State, changed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommunityCanUseOwnedBotAsInitialPeer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := NewUserStore()
|
||||
owner := mustCommunityTestUser(t, users, "Bot Owner", "15551000021")
|
||||
bots := NewBotStore(users)
|
||||
bot, _, err := bots.CreateBotAccount(ctx, domain.User{
|
||||
AccessHash: 91, FirstName: "Community Bot", Username: "community_bot",
|
||||
}, domain.BotProfile{OwnerUserID: owner.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("create bot: %v", err)
|
||||
}
|
||||
store := NewCommunityStore(users, NewChannelStore(), bots, nil)
|
||||
created, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Bot Community",
|
||||
InitialPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bot.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_000_200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create bot community: %v", err)
|
||||
}
|
||||
if len(created.Links) != 1 || created.Links[0].Peer.ID != bot.ID || !created.Links[0].CanViewHistory {
|
||||
t.Fatalf("bot community links = %+v", created.Links)
|
||||
}
|
||||
if len(created.ServiceMessages) != 0 {
|
||||
t.Fatalf("bot link service messages = %+v, want none", created.ServiceMessages)
|
||||
}
|
||||
updatedBot, ok, err := users.ByID(ctx, bot.ID)
|
||||
if err != nil || !ok || updatedBot.LinkedCommunityID != created.Community.ID {
|
||||
t.Fatalf("bot linked community = %d ok=%v err=%v", updatedBot.LinkedCommunityID, ok, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,11 +262,23 @@ func filterMessageList(messages []domain.Message, filter domain.MessageFilter) d
|
|||
})
|
||||
|
||||
query := strings.ToLower(filter.Query)
|
||||
peerIDs := make(map[int64]struct{}, len(filter.PeerIDs))
|
||||
for _, id := range filter.PeerIDs {
|
||||
peerIDs[id] = struct{}{}
|
||||
}
|
||||
base := make([]domain.Message, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
if filter.HasPeer && msg.Peer != filter.Peer {
|
||||
continue
|
||||
}
|
||||
if filter.RestrictPeerIDs {
|
||||
if msg.Peer.Type != domain.PeerTypeUser {
|
||||
continue
|
||||
}
|
||||
if _, ok := peerIDs[msg.Peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if query != "" && !strings.Contains(strings.ToLower(msg.Body), query) {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ func scanChannel(row rowScanner) (domain.Channel, error) {
|
|||
func channelScanDest(ch *domain.Channel, rights, reactionPolicy *string, wallpaper **string) []any {
|
||||
return []any{
|
||||
&ch.ID, &ch.AccessHash, &ch.CreatorUserID, &ch.Title, &ch.About, &ch.Username, &ch.Verified,
|
||||
&ch.Broadcast, &ch.Megagroup, &ch.Forum, &ch.ForumTabs, &ch.Autotranslation, &ch.RestrictedSponsored, &ch.BroadcastMessagesAllowed, &ch.SendPaidMessagesStars, &ch.NoForwards, &ch.JoinToSend, &ch.JoinRequest, &ch.Signatures, &ch.PreHistoryHidden, &ch.ParticipantsHidden, &ch.AntiSpam, &ch.HasLink, &ch.LinkedChatID, &ch.Monoforum, &ch.LinkedMonoforumID, &ch.SlowmodeSeconds, &ch.BoostsUnrestrict, rights,
|
||||
&ch.Broadcast, &ch.Megagroup, &ch.Forum, &ch.ForumTabs, &ch.Autotranslation, &ch.RestrictedSponsored, &ch.BroadcastMessagesAllowed, &ch.SendPaidMessagesStars, &ch.NoForwards, &ch.JoinToSend, &ch.JoinRequest, &ch.Signatures, &ch.PreHistoryHidden, &ch.ParticipantsHidden, &ch.AntiSpam, &ch.HasLink, &ch.LinkedChatID, &ch.LinkedCommunityID, &ch.Monoforum, &ch.LinkedMonoforumID, &ch.SlowmodeSeconds, &ch.BoostsUnrestrict, rights,
|
||||
reactionPolicy, &ch.Color.HasColor, &ch.Color.Color, &ch.Color.BackgroundEmojiID, &ch.ProfileColor.HasColor, &ch.ProfileColor.Color, &ch.ProfileColor.BackgroundEmojiID, &ch.EmojiStatus.DocumentID, &ch.EmojiStatus.Until,
|
||||
wallpaper, &ch.ParticipantsCount, &ch.AdminsCount, &ch.KickedCount, &ch.BannedCount, &ch.TopMessageID,
|
||||
&ch.PinnedMessageID, &ch.Pts, &ch.TTLPeriod, &ch.Date, &ch.Deleted,
|
||||
|
|
|
|||
|
|
@ -544,6 +544,40 @@ LIMIT $2`, userID, domain.MaxAdminedPublicChannels)
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListCommunityLinkableChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
|
||||
if userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT i.channel_id
|
||||
FROM user_channel_member_index i
|
||||
JOIN channels c ON c.id=i.channel_id
|
||||
WHERE i.user_id=$1
|
||||
AND i.status='active'
|
||||
AND i.role IN ('creator','admin')
|
||||
AND NOT i.deleted
|
||||
AND NOT c.monoforum
|
||||
AND c.linked_community_id=0
|
||||
ORDER BY i.channel_id DESC
|
||||
LIMIT $2`, userID, domain.MaxCommunityPeers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list community-linkable channels: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
ids := make([]int64, 0, domain.MaxCommunityPeers)
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listChannelsByIDsInOrder(ctx, s.db, ids)
|
||||
}
|
||||
|
||||
// ListSendAsChannels lists the broadcast channels a user may post messages AS in groups
|
||||
// (channels.getSendAs candidates): channels where the user is the creator, or an admin holding
|
||||
// PostMessages rights. Mirrors ListStoryPostableChannels but is restricted to broadcast channels
|
||||
|
|
|
|||
|
|
@ -221,8 +221,12 @@ func (s *ChannelStore) SearchJoinedMessages(ctx context.Context, viewerUserID in
|
|||
if limit <= 0 || limit > domain.MaxChannelGlobalSearchLimit {
|
||||
limit = domain.MaxChannelGlobalSearchLimit
|
||||
}
|
||||
args := []any{viewerUserID}
|
||||
args := []any{viewerUserID, req.AllowPublicPreview}
|
||||
where := `NOT deleted`
|
||||
if req.RestrictChannelIDs {
|
||||
args = append(args, req.ChannelIDs)
|
||||
where += fmt.Sprintf("\nAND channel_id = ANY($%d::bigint[])", len(args))
|
||||
}
|
||||
if query != "" {
|
||||
args = append(args, "%"+escapeLike(query)+"%")
|
||||
where += fmt.Sprintf(`
|
||||
|
|
@ -242,15 +246,18 @@ AND EXISTS (
|
|||
where += `
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM channels c
|
||||
JOIN channel_members cm ON cm.channel_id = c.id
|
||||
AND cm.user_id = $1
|
||||
AND cm.status = 'active'
|
||||
AND NOT COALESCE((cm.banned_rights->>'ViewMessages')::boolean, false)
|
||||
FROM channels c
|
||||
LEFT JOIN channel_members cm ON cm.channel_id = c.id AND cm.user_id = $1
|
||||
LEFT JOIN channel_dialogs d ON d.channel_id = c.id AND d.user_id = $1
|
||||
WHERE c.id = channel_messages.channel_id
|
||||
AND NOT c.deleted
|
||||
AND (cm.available_min_id <= 0 OR channel_messages.id > cm.available_min_id)`
|
||||
WHERE c.id = channel_messages.channel_id
|
||||
AND NOT c.deleted
|
||||
AND (
|
||||
(cm.status = 'active' AND NOT COALESCE((cm.banned_rights->>'ViewMessages')::boolean, false))
|
||||
OR ($2::boolean AND COALESCE(c.username,'') <> ''
|
||||
AND COALESCE(cm.status,'') <> 'kicked'
|
||||
AND NOT COALESCE((cm.banned_rights->>'ViewMessages')::boolean, false))
|
||||
)
|
||||
AND (COALESCE(cm.status,'') <> 'active' OR cm.available_min_id <= 0 OR channel_messages.id > cm.available_min_id)`
|
||||
if req.BroadcastsOnly {
|
||||
where += `
|
||||
AND c.broadcast AND NOT c.megagroup`
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func NewChannelStore(db sqlcgen.DBTX, opts ...ChannelStoreOption) *ChannelStore
|
|||
const channelColumns = `c.id, c.access_hash, c.creator_user_id, c.title, c.about, COALESCE(c.username, ''), c.verified,
|
||||
c.broadcast, c.megagroup, c.forum, c.forum_tabs, c.autotranslation, c.restricted_sponsored, c.broadcast_messages_allowed, c.send_paid_messages_stars, c.noforwards, c.join_to_send, c.join_request, c.signatures, c.pre_history_hidden, c.participants_hidden, c.antispam,
|
||||
EXISTS (SELECT 1 FROM channel_invites ci WHERE ci.channel_id = c.id AND NOT ci.revoked) AS has_link,
|
||||
c.linked_chat_id, c.monoforum, c.linked_monoforum_id, c.slowmode_seconds, c.boosts_unrestrict, c.default_banned_rights::text,
|
||||
c.linked_chat_id, c.linked_community_id, c.monoforum, c.linked_monoforum_id, c.slowmode_seconds, c.boosts_unrestrict, c.default_banned_rights::text,
|
||||
c.available_reactions::text, c.color_set, c.color, c.color_background_emoji_id, c.profile_color_set, c.profile_color, c.profile_color_background_emoji_id, c.emoji_status_document_id, c.emoji_status_until,
|
||||
c.wallpaper::text, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts,
|
||||
c.ttl_period, c.date, c.deleted, c.photo_id, c.photo_dc_id, c.photo_stripped,
|
||||
|
|
|
|||
1374
internal/store/postgres/community.go
Normal file
1374
internal/store/postgres/community.go
Normal file
File diff suppressed because it is too large
Load diff
138
internal/store/postgres/community_integration_test.go
Normal file
138
internal/store/postgres/community_integration_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestCommunityStoreLifecycleIsAtomicInPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 801, Phone: "+1888" + suffix + "01", FirstName: "CommunityOwner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
member, err := users.Create(ctx, domain.User{AccessHash: 802, Phone: "+1888" + suffix + "02", FirstName: "SearchableMember"})
|
||||
if err != nil {
|
||||
t.Fatalf("create member: %v", err)
|
||||
}
|
||||
channels := NewChannelStore(pool)
|
||||
initial, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Community Initial " + suffix,
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{member.ID},
|
||||
Date: 1_800_200_000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create initial channel: %v", err)
|
||||
}
|
||||
owned, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: member.ID,
|
||||
Title: "Community Owned " + suffix,
|
||||
Megagroup: true,
|
||||
Date: 1_800_200_001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create owned channel: %v", err)
|
||||
}
|
||||
owned.Channel, err = channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
||||
UserID: member.ID, ChannelID: owned.Channel.ID, Username: "communitypreview" + suffix,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("make owned channel public: %v", err)
|
||||
}
|
||||
if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: member.ID, ChannelID: owned.Channel.ID, RandomID: 9_900_000_001,
|
||||
Message: "community public preview search", Date: 1_800_200_001,
|
||||
}); err != nil {
|
||||
t.Fatalf("send public preview message: %v", err)
|
||||
}
|
||||
var communityID int64
|
||||
t.Cleanup(func() {
|
||||
if communityID != 0 {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM communities WHERE id=$1", communityID)
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id=ANY($1::bigint[])", []int64{initial.Channel.ID, owned.Channel.ID})
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id=ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
||||
})
|
||||
|
||||
store := NewCommunityStore(pool, nil, nil)
|
||||
created, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Postgres Community " + suffix,
|
||||
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: initial.Channel.ID},
|
||||
Visibility: domain.CommunityPeerHidden,
|
||||
Date: 1_800_200_002,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create community: %v", err)
|
||||
}
|
||||
communityID = created.Community.ID
|
||||
if len(created.ServiceMessages) != 1 || created.ServiceMessages[0].Event.Pts == 0 {
|
||||
t.Fatalf("create service messages = %+v", created.ServiceMessages)
|
||||
}
|
||||
var linkedID int64
|
||||
if err := pool.QueryRow(ctx, "SELECT linked_community_id FROM channels WHERE id=$1", initial.Channel.ID).Scan(&linkedID); err != nil || linkedID != communityID {
|
||||
t.Fatalf("initial linked_community_id = %d err=%v, want %d", linkedID, err, communityID)
|
||||
}
|
||||
|
||||
requested, err := store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
||||
ActorUserID: member.ID,
|
||||
CommunityID: communityID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: owned.Channel.ID},
|
||||
Visibility: domain.CommunityPeerVisible,
|
||||
Date: 1_800_200_003,
|
||||
})
|
||||
if err != nil || !requested.RequestCreated {
|
||||
t.Fatalf("create peer link request = %+v err=%v", requested, err)
|
||||
}
|
||||
approved, err := store.DecideCommunityPeerLinkRequest(ctx, owner.ID, communityID, requested.Peer, false, 1_800_200_004)
|
||||
if err != nil || approved.Link == nil || approved.RequestedBy != member.ID || approved.ServiceMessage == nil {
|
||||
t.Fatalf("approve link request = %+v err=%v", approved, err)
|
||||
}
|
||||
search, err := channels.SearchJoinedMessages(ctx, owner.ID, domain.ChannelGlobalSearchRequest{
|
||||
Query: "public preview", ChannelIDs: []int64{owned.Channel.ID}, RestrictChannelIDs: true,
|
||||
AllowPublicPreview: true, Limit: 20,
|
||||
})
|
||||
if err != nil || len(search.Messages) != 1 || search.Messages[0].ChannelID != owned.Channel.ID {
|
||||
t.Fatalf("community public-preview search = %+v err=%v", search.Messages, err)
|
||||
}
|
||||
|
||||
participants, err := store.ListCommunityParticipants(ctx, owner.ID, communityID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsSearch, Query: "SEARCHABLE",
|
||||
}, 0, 20)
|
||||
if err != nil || participants.Count != 1 || len(participants.Participants) != 1 || participants.Participants[0].UserID != member.ID {
|
||||
t.Fatalf("participant search = %+v err=%v", participants, err)
|
||||
}
|
||||
admins, err := store.ListCommunityParticipants(ctx, member.ID, communityID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsAdmins,
|
||||
}, 0, 100)
|
||||
if err != nil || admins.Count != 1 || len(admins.Participants) != 1 || admins.Participants[0].UserID != owner.ID {
|
||||
t.Fatalf("member-visible Community admins = %+v err=%v", admins, err)
|
||||
}
|
||||
if _, err := store.ListCommunityParticipants(ctx, member.ID, communityID, domain.ChannelParticipantsFilter{
|
||||
Kind: domain.ChannelParticipantsBanned,
|
||||
}, 0, 100); !errors.Is(err, domain.ErrCommunityAdminRequired) {
|
||||
t.Fatalf("member Community banned list error = %v, want admin required", err)
|
||||
}
|
||||
|
||||
ban, err := store.ToggleCommunityParticipantBanned(ctx, owner.ID, communityID, member.ID, false, 1_800_200_005)
|
||||
if err != nil {
|
||||
t.Fatalf("ban participant: %v", err)
|
||||
}
|
||||
if !ban.Changed || len(ban.ChannelBans) != 1 || len(ban.RemovedLinks) != 1 {
|
||||
t.Fatalf("ban result = %+v", ban)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, "SELECT linked_community_id FROM channels WHERE id=$1", owned.Channel.ID).Scan(&linkedID); err != nil || linkedID != 0 {
|
||||
t.Fatalf("owned linked_community_id after ban = %d err=%v, want 0", linkedID, err)
|
||||
}
|
||||
if _, err := store.GetCommunity(ctx, member.ID, communityID); !errors.Is(err, domain.ErrCommunityPrivate) {
|
||||
t.Fatalf("banned member get community error = %v, want private", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -100,21 +100,23 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
var rows []sqlcgen.ListMessagesByUserRow
|
||||
if addOffset >= 0 {
|
||||
bw, err := s.q.ListMessagesBackward(ctx, sqlcgen.ListMessagesBackwardParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
RowOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
RowOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("list messages (backward): %w", err)
|
||||
|
|
@ -125,17 +127,19 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
}
|
||||
if filter.NeedTotalCount {
|
||||
total, err := s.q.CountMessagesByUser(ctx, sqlcgen.CountMessagesByUserParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("count messages: %w", err)
|
||||
|
|
@ -149,22 +153,24 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
} else {
|
||||
var err error
|
||||
rows, err = s.q.ListMessagesByUser(ctx, sqlcgen.ListMessagesByUserParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
Query: filter.Query,
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
AddOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
NeedTotalCount: filter.NeedTotalCount,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
AddOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
NeedTotalCount: filter.NeedTotalCount,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("list messages: %w", err)
|
||||
|
|
|
|||
|
|
@ -537,6 +537,10 @@ base AS NOT MATERIALIZED (
|
|||
NOT sqlc.arg(has_peer)::boolean
|
||||
OR (m.peer_type = sqlc.arg(peer_type)::text AND m.peer_id = sqlc.arg(peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
NOT sqlc.arg(restrict_peer_ids)::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY(sqlc.arg(peer_ids)::bigint[]))
|
||||
)
|
||||
AND (
|
||||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
|
|
@ -798,6 +802,10 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
NOT sqlc.arg(has_peer)::boolean
|
||||
OR (m.peer_type = sqlc.arg(peer_type)::text AND m.peer_id = sqlc.arg(peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
NOT sqlc.arg(restrict_peer_ids)::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY(sqlc.arg(peer_ids)::bigint[]))
|
||||
)
|
||||
AND (
|
||||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
|
|
@ -840,6 +848,10 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
NOT sqlc.arg(has_peer)::boolean
|
||||
OR (m.peer_type = sqlc.arg(peer_type)::text AND m.peer_id = sqlc.arg(peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
NOT sqlc.arg(restrict_peer_ids)::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY(sqlc.arg(peer_ids)::bigint[]))
|
||||
)
|
||||
AND (
|
||||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ WITH matched AS (
|
|||
u.profile_color_set,
|
||||
u.profile_color,
|
||||
u.profile_color_background_emoji_id,
|
||||
u.linked_community_id,
|
||||
u.last_seen_at,
|
||||
(c.contact_user_id IS NOT NULL)::boolean AS contact,
|
||||
COALESCE(c.mutual, false)::boolean AS mutual,
|
||||
|
|
@ -96,6 +97,7 @@ SELECT
|
|||
profile_color_set,
|
||||
profile_color,
|
||||
profile_color_background_emoji_id,
|
||||
linked_community_id,
|
||||
last_seen_at,
|
||||
contact,
|
||||
mutual
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ func (q *Queries) InsertBot(ctx context.Context, arg InsertBotParams) error {
|
|||
const insertBotUser = `-- name: InsertBotUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, is_bot, bot_info_version)
|
||||
VALUES ($1, '', $2, '', $3, '', TRUE, 1)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type InsertBotUserParams struct {
|
||||
|
|
@ -215,6 +215,7 @@ func (q *Queries) InsertBotUser(ctx context.Context, arg InsertBotUserParams) (U
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,18 @@ WHERE m.owner_user_id = $1::bigint
|
|||
OR (m.peer_type = $3::text AND m.peer_id = $4::bigint)
|
||||
)
|
||||
AND (
|
||||
$5::text = ''
|
||||
OR m.body ILIKE ('%' || $5::text || '%')
|
||||
NOT $5::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY($6::bigint[]))
|
||||
)
|
||||
AND ($6::int <= 0 OR m.box_id < $6::int)
|
||||
AND ($7::int <= 0 OR m.box_id > $7::int)
|
||||
AND (NOT $8::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $9::boolean
|
||||
$7::text = ''
|
||||
OR m.body ILIKE ('%' || $7::text || '%')
|
||||
)
|
||||
AND ($8::int <= 0 OR m.box_id < $8::int)
|
||||
AND ($9::int <= 0 OR m.box_id > $9::int)
|
||||
AND (NOT $10::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $11::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -38,23 +42,25 @@ WHERE m.owner_user_id = $1::bigint
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$10::text = ''
|
||||
OR (m.saved_peer_type = $10::text AND m.saved_peer_id = $11::bigint)
|
||||
$12::text = ''
|
||||
OR (m.saved_peer_type = $12::text AND m.saved_peer_id = $13::bigint)
|
||||
)
|
||||
`
|
||||
|
||||
type CountMessagesByUserParams struct {
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
}
|
||||
|
||||
// ListMessagesByUser total CTE 的独立化:相同 base 过滤(不含分页 anchor),
|
||||
|
|
@ -65,6 +71,8 @@ func (q *Queries) CountMessagesByUser(ctx context.Context, arg CountMessagesByUs
|
|||
arg.HasPeer,
|
||||
arg.PeerType,
|
||||
arg.PeerID,
|
||||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
|
|
@ -2280,14 +2288,18 @@ WHERE m.owner_user_id = $1::bigint
|
|||
OR (m.peer_type = $3::text AND m.peer_id = $4::bigint)
|
||||
)
|
||||
AND (
|
||||
$5::text = ''
|
||||
OR m.body ILIKE ('%' || $5::text || '%')
|
||||
NOT $5::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY($6::bigint[]))
|
||||
)
|
||||
AND ($6::int <= 0 OR m.box_id < $6::int)
|
||||
AND ($7::int <= 0 OR m.box_id > $7::int)
|
||||
AND (NOT $8::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $9::boolean
|
||||
$7::text = ''
|
||||
OR m.body ILIKE ('%' || $7::text || '%')
|
||||
)
|
||||
AND ($8::int <= 0 OR m.box_id < $8::int)
|
||||
AND ($9::int <= 0 OR m.box_id > $9::int)
|
||||
AND (NOT $10::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $11::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -2299,34 +2311,36 @@ WHERE m.owner_user_id = $1::bigint
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$10::text = ''
|
||||
OR (m.saved_peer_type = $10::text AND m.saved_peer_id = $11::bigint)
|
||||
$12::text = ''
|
||||
OR (m.saved_peer_type = $12::text AND m.saved_peer_id = $13::bigint)
|
||||
)
|
||||
AND (
|
||||
($12::int > 0 AND m.message_date < $12::int)
|
||||
OR ($12::int <= 0 AND ($13::int <= 0 OR m.box_id < $13::int))
|
||||
($14::int > 0 AND m.message_date < $14::int)
|
||||
OR ($14::int <= 0 AND ($15::int <= 0 OR m.box_id < $15::int))
|
||||
)
|
||||
ORDER BY m.box_id DESC
|
||||
OFFSET GREATEST($14::int, 0)
|
||||
LIMIT $15::int
|
||||
OFFSET GREATEST($16::int, 0)
|
||||
LIMIT $17::int
|
||||
`
|
||||
|
||||
type ListMessagesBackwardParams struct {
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
OffsetDate int32
|
||||
OffsetID int32
|
||||
RowOffset int32
|
||||
LimitCount int32
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
OffsetDate int32
|
||||
OffsetID int32
|
||||
RowOffset int32
|
||||
LimitCount int32
|
||||
}
|
||||
|
||||
type ListMessagesBackwardRow struct {
|
||||
|
|
@ -2416,6 +2430,8 @@ func (q *Queries) ListMessagesBackward(ctx context.Context, arg ListMessagesBack
|
|||
arg.HasPeer,
|
||||
arg.PeerType,
|
||||
arg.PeerID,
|
||||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
|
|
@ -2618,14 +2634,18 @@ base AS NOT MATERIALIZED (
|
|||
OR (m.peer_type = $7::text AND m.peer_id = $8::bigint)
|
||||
)
|
||||
AND (
|
||||
$9::text = ''
|
||||
OR m.body ILIKE ('%' || $9::text || '%')
|
||||
NOT $9::boolean
|
||||
OR (m.peer_type = 'user' AND m.peer_id = ANY($10::bigint[]))
|
||||
)
|
||||
AND ($10::int <= 0 OR m.box_id < $10::int)
|
||||
AND ($11::int <= 0 OR m.box_id > $11::int)
|
||||
AND (NOT $12::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $13::boolean
|
||||
$11::text = ''
|
||||
OR m.body ILIKE ('%' || $11::text || '%')
|
||||
)
|
||||
AND ($12::int <= 0 OR m.box_id < $12::int)
|
||||
AND ($13::int <= 0 OR m.box_id > $13::int)
|
||||
AND (NOT $14::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $15::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -2637,14 +2657,14 @@ base AS NOT MATERIALIZED (
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$14::text = ''
|
||||
OR (m.saved_peer_type = $14::text AND m.saved_peer_id = $15::bigint)
|
||||
$16::text = ''
|
||||
OR (m.saved_peer_type = $16::text AND m.saved_peer_id = $17::bigint)
|
||||
)
|
||||
),
|
||||
total AS (
|
||||
SELECT count(*)::int AS total_count
|
||||
FROM base
|
||||
WHERE $16::boolean
|
||||
WHERE $18::boolean
|
||||
),
|
||||
backward AS (
|
||||
SELECT b.box_id, b.private_message_id, b.owner_user_id, b.peer_type, b.peer_id, b.from_user_id, b.message_date, b.ttl_period, b.expires_at, b.edit_date, b.hide_edited, b.outgoing, b.body, b.entities_json, b.silent, b.noforwards, b.reply_to_msg_id, b.reply_to_peer_type, b.reply_to_peer_id, b.reply_to_top_id, b.reply_to_story_id, b.quote_text, b.quote_entities_json, b.quote_offset, b.fwd_from_peer_type, b.fwd_from_peer_id, b.fwd_from_name, b.fwd_date, b.fwd_saved_from_peer_type, b.fwd_saved_from_peer_id, b.fwd_saved_from_msg_id, b.saved_peer_type, b.saved_peer_id, b.pts, b.media_json, b.media_unread, b.reaction_unread, b.pinned, b.via_bot_id, b.grouped_id, b.effect, b.reply_markup_json, b.rich_message_json, b.peer_user_id, b.peer_access_hash, b.peer_phone, b.peer_first_name, b.peer_last_name, b.peer_username, b.peer_country_code, b.peer_verified, b.peer_support, b.peer_is_bot, b.peer_bot_info_version, b.peer_premium_until, b.peer_emoji_status_document_id, b.peer_emoji_status_until, b.peer_last_seen_at, b.from_user_user_id, b.from_user_access_hash, b.from_user_phone, b.from_user_first_name, b.from_user_last_name, b.from_user_username, b.from_user_country_code, b.from_user_verified, b.from_user_support, b.from_user_is_bot, b.from_user_bot_info_version, b.from_user_premium_until, b.from_user_emoji_status_document_id, b.from_user_emoji_status_until, b.from_user_last_seen_at
|
||||
|
|
@ -2791,22 +2811,24 @@ ORDER BY box_id DESC
|
|||
`
|
||||
|
||||
type ListMessagesByUserParams struct {
|
||||
OwnerUserID int64
|
||||
OffsetID int32
|
||||
OffsetDate int32
|
||||
AddOffset int32
|
||||
LimitCount int32
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
NeedTotalCount bool
|
||||
OwnerUserID int64
|
||||
OffsetID int32
|
||||
OffsetDate int32
|
||||
AddOffset int32
|
||||
LimitCount int32
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
NeedTotalCount bool
|
||||
}
|
||||
|
||||
type ListMessagesByUserRow struct {
|
||||
|
|
@ -2896,6 +2918,8 @@ func (q *Queries) ListMessagesByUser(ctx context.Context, arg ListMessagesByUser
|
|||
arg.HasPeer,
|
||||
arg.PeerType,
|
||||
arg.PeerID,
|
||||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ type BotApiUpdate struct {
|
|||
CallbackInlineOwnerID int64
|
||||
CallbackInlineMessageID int32
|
||||
CallbackInlineAccessHash int64
|
||||
EphemeralPayload []byte
|
||||
}
|
||||
|
||||
type BotApiUpdateState struct {
|
||||
|
|
@ -323,6 +324,21 @@ type BotApiUpdateState struct {
|
|||
PollExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type BotApiWebhook struct {
|
||||
BotUserID int64
|
||||
Url string
|
||||
SecretToken string
|
||||
MaxConnections int32
|
||||
AllowedUpdates []string
|
||||
FailureCount int32
|
||||
LastErrorDate int32
|
||||
LastErrorMessage string
|
||||
NextAttemptAt pgtype.Timestamptz
|
||||
DeliveryOwner string
|
||||
DeliveryExpiresAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type BotApp struct {
|
||||
ID int64
|
||||
BotUserID int64
|
||||
|
|
@ -487,6 +503,7 @@ type Channel struct {
|
|||
LinkedMonoforumID int64
|
||||
Wallpaper []byte
|
||||
Verified bool
|
||||
LinkedCommunityID int64
|
||||
}
|
||||
|
||||
type ChannelAdminLogEvent struct {
|
||||
|
|
@ -844,6 +861,62 @@ type ChatlistMembership struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Community struct {
|
||||
ID int64
|
||||
AccessHash int64
|
||||
CreatorUserID int64
|
||||
Title string
|
||||
About string
|
||||
DefaultBannedRights []byte
|
||||
PhotoID int64
|
||||
PhotoDcID int32
|
||||
PhotoStripped []byte
|
||||
Date int32
|
||||
Deleted bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CommunityMember struct {
|
||||
CommunityID int64
|
||||
UserID int64
|
||||
Role string
|
||||
Status string
|
||||
AdminRights []byte
|
||||
Rank string
|
||||
Date int32
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CommunityPeerLink struct {
|
||||
CommunityID int64
|
||||
PeerType string
|
||||
PeerID int64
|
||||
Visibility string
|
||||
CreatedBy int64
|
||||
Date int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CommunityPeerLinkRequest struct {
|
||||
CommunityID int64
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RequestedBy int64
|
||||
Visibility string
|
||||
Date int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CommunityUserState struct {
|
||||
CommunityID int64
|
||||
UserID int64
|
||||
Collapsed bool
|
||||
Pinned bool
|
||||
PinnedOrder int32
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
UserID int64
|
||||
ContactUserID int64
|
||||
|
|
@ -1019,6 +1092,21 @@ type EncryptedStateEventDelivery struct {
|
|||
AuthKeyID int64
|
||||
}
|
||||
|
||||
type EphemeralAbuseReport struct {
|
||||
ID int64
|
||||
ReporterUserID int64
|
||||
ChannelID int64
|
||||
EphemeralMessageID int32
|
||||
SenderUserID int64
|
||||
ReceiverUserID int64
|
||||
ReportOption string
|
||||
ReportComment string
|
||||
CommentHash []byte
|
||||
PayloadHash []byte
|
||||
Evidence []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type FileBlob struct {
|
||||
LocationKey string
|
||||
Backend string
|
||||
|
|
@ -1785,6 +1873,18 @@ type StarGiftOffer struct {
|
|||
ResolutionNotified bool
|
||||
}
|
||||
|
||||
type StarGiftPatternDocumentRepair struct {
|
||||
OldDocumentID int64
|
||||
NewDocumentID int64
|
||||
RepairedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftPatternPreviewDocumentRepair struct {
|
||||
OldDocumentID int64
|
||||
NewDocumentID int64
|
||||
RepairedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftPrepaidUpgradeCommand struct {
|
||||
PayerUserID int64
|
||||
CommandKey string
|
||||
|
|
@ -2000,6 +2100,28 @@ type StoryView struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type TelesrvCollectiblePatternCorrectionEvent struct {
|
||||
UserID int64
|
||||
Pts int32
|
||||
}
|
||||
|
||||
type TelesrvPatternPreviewCorrectionEvent struct {
|
||||
UserID int64
|
||||
Pts int32
|
||||
}
|
||||
|
||||
type TelesrvPatternPreviewRepairedWearer struct {
|
||||
UserID int64
|
||||
OldDocumentID int64
|
||||
NewDocumentID int64
|
||||
}
|
||||
|
||||
type TelesrvRepairedCollectibleWearer struct {
|
||||
UserID int64
|
||||
OldDocumentID int64
|
||||
NewDocumentID int64
|
||||
}
|
||||
|
||||
type TempAuthKeyBinding struct {
|
||||
TempAuthKeyID int64
|
||||
PermAuthKeyID int64
|
||||
|
|
@ -2160,6 +2282,7 @@ type User struct {
|
|||
AccountDeleteAt pgtype.Timestamptz
|
||||
EmojiStatusCollectibleID *int64
|
||||
EmojiStatusCollectible []byte
|
||||
LinkedCommunityID int64
|
||||
}
|
||||
|
||||
type UserBusinessProfile struct {
|
||||
|
|
@ -2298,13 +2421,17 @@ type WebviewCustomMethodQuery struct {
|
|||
}
|
||||
|
||||
type WebviewRequestedButton struct {
|
||||
WebappReqID string
|
||||
BotUserID int64
|
||||
UserID int64
|
||||
ButtonID int32
|
||||
Text string
|
||||
PeerType string
|
||||
MaxQuantity int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
WebappReqID string
|
||||
BotUserID int64
|
||||
UserID int64
|
||||
ButtonID int32
|
||||
Text string
|
||||
PeerType string
|
||||
MaxQuantity int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
PeerFilter []byte
|
||||
NameRequested bool
|
||||
UsernameRequested bool
|
||||
PhotoRequested bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
|
|
@ -74,12 +74,13 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible FROM users WHERE id = $1
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
|
|
@ -121,12 +122,13 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByPhone = `-- name: GetUserByPhone :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible FROM users WHERE phone = $1 AND deleted_at IS NULL
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id FROM users WHERE phone = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error) {
|
||||
|
|
@ -168,12 +170,13 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible FROM users WHERE lower(username) = lower($1) AND username <> '' AND deleted_at IS NULL
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id FROM users WHERE lower(username) = lower($1) AND username <> '' AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, error) {
|
||||
|
|
@ -215,12 +218,13 @@ func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, er
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsersByIDs = `-- name: GetUsersByIDs :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
FROM users
|
||||
WHERE id = ANY($1::bigint[])
|
||||
ORDER BY id
|
||||
|
|
@ -271,6 +275,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -283,7 +288,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
}
|
||||
|
||||
const getUsersByPhones = `-- name: GetUsersByPhones :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
FROM users
|
||||
WHERE phone = ANY($1::text[]) AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
|
|
@ -334,6 +339,7 @@ func (q *Queries) GetUsersByPhones(ctx context.Context, phones []string) ([]User
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -371,6 +377,7 @@ WITH matched AS (
|
|||
u.profile_color_set,
|
||||
u.profile_color,
|
||||
u.profile_color_background_emoji_id,
|
||||
u.linked_community_id,
|
||||
u.last_seen_at,
|
||||
(c.contact_user_id IS NOT NULL)::boolean AS contact,
|
||||
COALESCE(c.mutual, false)::boolean AS mutual,
|
||||
|
|
@ -422,6 +429,7 @@ SELECT
|
|||
profile_color_set,
|
||||
profile_color,
|
||||
profile_color_background_emoji_id,
|
||||
linked_community_id,
|
||||
last_seen_at,
|
||||
contact,
|
||||
mutual
|
||||
|
|
@ -462,6 +470,7 @@ type SearchUsersRow struct {
|
|||
ProfileColorSet bool
|
||||
ProfileColor int32
|
||||
ProfileColorBackgroundEmojiID int64
|
||||
LinkedCommunityID int64
|
||||
LastSeenAt int64
|
||||
Contact bool
|
||||
Mutual bool
|
||||
|
|
@ -506,6 +515,7 @@ func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]Sea
|
|||
&i.ProfileColorSet,
|
||||
&i.ProfileColor,
|
||||
&i.ProfileColorBackgroundEmojiID,
|
||||
&i.LinkedCommunityID,
|
||||
&i.LastSeenAt,
|
||||
&i.Contact,
|
||||
&i.Mutual,
|
||||
|
|
@ -525,7 +535,7 @@ UPDATE users
|
|||
SET premium_expires_at = $1::timestamptz,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type SetUserPremiumUntilParams struct {
|
||||
|
|
@ -572,6 +582,7 @@ func (q *Queries) SetUserPremiumUntil(ctx context.Context, arg SetUserPremiumUnt
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -581,7 +592,7 @@ UPDATE users
|
|||
SET verified = $1::boolean,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type SetUserVerifiedParams struct {
|
||||
|
|
@ -628,6 +639,7 @@ func (q *Queries) SetUserVerified(ctx context.Context, arg SetUserVerifiedParams
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -644,7 +656,7 @@ WHERE id IN (
|
|||
ORDER BY premium_expires_at
|
||||
LIMIT $2::int
|
||||
)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type SweepExpiredPremiumParams struct {
|
||||
|
|
@ -697,6 +709,7 @@ func (q *Queries) SweepExpiredPremium(ctx context.Context, arg SweepExpiredPremi
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -715,7 +728,7 @@ SET birthday_day = $1::int,
|
|||
birthday_year = $3::int,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserBirthdayParams struct {
|
||||
|
|
@ -769,6 +782,7 @@ func (q *Queries) UpdateUserBirthday(ctx context.Context, arg UpdateUserBirthday
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -780,7 +794,7 @@ SET color_set = $1::boolean,
|
|||
color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserColorParams struct {
|
||||
|
|
@ -834,6 +848,7 @@ func (q *Queries) UpdateUserColor(ctx context.Context, arg UpdateUserColorParams
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -846,7 +861,7 @@ SET emoji_status_document_id = $1::bigint,
|
|||
emoji_status_collectible = $4::jsonb,
|
||||
updated_at = now()
|
||||
WHERE id = $5::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserEmojiStatusParams struct {
|
||||
|
|
@ -902,6 +917,7 @@ func (q *Queries) UpdateUserEmojiStatus(ctx context.Context, arg UpdateUserEmoji
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -928,7 +944,7 @@ UPDATE users
|
|||
SET personal_channel_id = $1::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserPersonalChannelParams struct {
|
||||
|
|
@ -975,6 +991,7 @@ func (q *Queries) UpdateUserPersonalChannel(ctx context.Context, arg UpdateUserP
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -984,7 +1001,7 @@ UPDATE users
|
|||
SET phone = $1::text,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserPhoneParams struct {
|
||||
|
|
@ -1031,6 +1048,7 @@ func (q *Queries) UpdateUserPhone(ctx context.Context, arg UpdateUserPhoneParams
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1042,7 +1060,7 @@ SET first_name = $2,
|
|||
about = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
|
|
@ -1096,6 +1114,7 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1107,7 +1126,7 @@ SET profile_color_set = $1::boolean,
|
|||
profile_color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserProfileColorParams struct {
|
||||
|
|
@ -1161,6 +1180,7 @@ func (q *Queries) UpdateUserProfileColor(ctx context.Context, arg UpdateUserProf
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1170,7 +1190,7 @@ UPDATE users
|
|||
SET username = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at, emoji_status_collectible_id, emoji_status_collectible, linked_community_id
|
||||
`
|
||||
|
||||
type UpdateUserUsernameParams struct {
|
||||
|
|
@ -1217,6 +1237,7 @@ func (q *Queries) UpdateUserUsername(ctx context.Context, arg UpdateUserUsername
|
|||
&i.AccountDeleteAt,
|
||||
&i.EmojiStatusCollectibleID,
|
||||
&i.EmojiStatusCollectible,
|
||||
&i.LinkedCommunityID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ func (s *UserStore) Search(ctx context.Context, currentUserID int64, query, phon
|
|||
EmojiStatusCollectible: collectible,
|
||||
Color: peerColorFromModel(row.ColorSet, row.Color, row.ColorBackgroundEmojiID),
|
||||
ProfileColor: peerColorFromModel(row.ProfileColorSet, row.ProfileColor, row.ProfileColorBackgroundEmojiID),
|
||||
LinkedCommunityID: row.LinkedCommunityID,
|
||||
LastSeenAt: int(row.LastSeenAt),
|
||||
Contact: row.Contact,
|
||||
Mutual: row.Mutual,
|
||||
|
|
@ -555,6 +556,7 @@ func userFromModel(r sqlcgen.User) domain.User {
|
|||
EmojiStatusCollectible: collectible,
|
||||
Birthday: domain.Birthday{Day: int(r.BirthdayDay), Month: int(r.BirthdayMonth), Year: int(r.BirthdayYear)},
|
||||
PersonalChannelID: r.PersonalChannelID,
|
||||
LinkedCommunityID: r.LinkedCommunityID,
|
||||
Color: peerColorFromModel(r.ColorSet, r.Color, r.ColorBackgroundEmojiID),
|
||||
ProfileColor: peerColorFromModel(r.ProfileColorSet, r.ProfileColor, r.ProfileColorBackgroundEmojiID),
|
||||
LastSeenAt: int(r.LastSeenAt),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue