fix: align private and channel update semantics

(cherry picked from commit c65f76f56278f74082c4fa792ed49104d5d33c38)
This commit is contained in:
A 2026-06-07 22:22:23 +08:00
parent dce7b92772
commit d84fa6e126
36 changed files with 1765 additions and 382 deletions

View file

@ -1375,6 +1375,17 @@ func (s *Service) ActiveChannelIDsForUser(ctx context.Context, userID, afterChan
return s.channels.ListActiveChannelIDsForUser(ctx, userID, afterChannelID, limit)
}
// DirtyActiveChannelsForUser pages active joined channels with channel events after sinceDate.
func (s *Service) DirtyActiveChannelsForUser(ctx context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error) {
if s == nil || s.channels == nil || userID == 0 || sinceDate <= 0 || afterChannelID < 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
return s.channels.ListDirtyActiveChannelsForUser(ctx, userID, sinceDate, afterChannelID, limit)
}
// ActiveMemberIDs returns a bounded list for transient online fanout such as typing.
func (s *Service) ActiveMemberIDs(ctx context.Context, userID, channelID int64, limit int) ([]int64, error) {
if s == nil || s.channels == nil || userID == 0 || channelID == 0 {

View file

@ -113,6 +113,7 @@ func TestChannelUnreadMentionsArePagedAndCleared(t *testing.T) {
ChannelID: created.Channel.ID,
RandomID: 9101,
Message: "hello @friend",
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindDocument},
MentionUserIDs: []int64{1002, 1002, 1001},
Date: 1700000101,
})
@ -133,6 +134,31 @@ func TestChannelUnreadMentionsArePagedAndCleared(t *testing.T) {
if other.Dialog.UnreadMentions != 0 {
t.Fatalf("unmentioned dialog unread mentions = %d, want 0", other.Dialog.UnreadMentions)
}
history, err := service.GetHistory(ctx, 1002, domain.ChannelHistoryFilter{ChannelID: created.Channel.ID, Limit: 10})
if err != nil {
t.Fatalf("GetHistory mentioned: %v", err)
}
if len(history.Messages) == 0 || !history.Messages[0].Mentioned || !history.Messages[0].MediaUnread {
t.Fatalf("mentioned history = %+v, want mentioned/media_unread flags", history.Messages)
}
otherHistory, err := service.GetHistory(ctx, 1003, domain.ChannelHistoryFilter{ChannelID: created.Channel.ID, Limit: 10})
if err != nil {
t.Fatalf("GetHistory other: %v", err)
}
if len(otherHistory.Messages) == 0 || otherHistory.Messages[0].Mentioned || otherHistory.Messages[0].MediaUnread {
t.Fatalf("other history = %+v, want no viewer-specific mention flags", otherHistory.Messages)
}
diff, err := service.GetDifference(ctx, 1002, domain.ChannelDifferenceRequest{
ChannelID: created.Channel.ID,
Pts: sent.Event.Pts - 1,
Limit: 10,
})
if err != nil {
t.Fatalf("GetDifference mentioned: %v", err)
}
if len(diff.NewMessages) != 1 || !diff.NewMessages[0].Mentioned || !diff.NewMessages[0].MediaUnread {
t.Fatalf("mentioned diff = %+v, want mentioned/media_unread flags", diff.NewMessages)
}
mentions, err := service.GetUnreadMentions(ctx, 1002, domain.ChannelUnreadMentionsFilter{
ChannelID: created.Channel.ID,
OffsetID: 1,
@ -159,6 +185,13 @@ func TestChannelUnreadMentionsArePagedAndCleared(t *testing.T) {
if mentions.Count != 0 || len(mentions.Messages) != 0 {
t.Fatalf("mentions after read = count %d messages %d, want empty", mentions.Count, len(mentions.Messages))
}
history, err = service.GetHistory(ctx, 1002, domain.ChannelHistoryFilter{ChannelID: created.Channel.ID, Limit: 10})
if err != nil {
t.Fatalf("GetHistory after read mentions: %v", err)
}
if len(history.Messages) == 0 || history.Messages[0].Mentioned || history.Messages[0].MediaUnread {
t.Fatalf("mentioned history after read = %+v, want mention flags cleared", history.Messages)
}
}
func TestServiceRejectsMismatchedUserContextForStateReads(t *testing.T) {

View file

@ -229,13 +229,59 @@ func (s *Service) GetPeerSettings(ctx context.Context, userID int64, peer domain
if err != nil {
return domain.PeerSettings{}, err
}
blocked, err := s.contacts.IsBlocked(ctx, userID, peer.ID)
if err != nil {
return domain.PeerSettings{}, err
}
return domain.PeerSettings{
AddContact: !found,
BlockContact: !found,
BlockContact: !blocked,
ShareContact: found,
}, nil
}
// BlockContact adds peer to the current user's blocklist.
func (s *Service) BlockContact(ctx context.Context, userID, peerUserID int64, date int) (bool, error) {
if s == nil || s.contacts == nil || userID == 0 || peerUserID == 0 || peerUserID == userID {
return false, ErrContactIDInvalid
}
if s.users != nil {
if _, found, err := s.users.ByID(ctx, peerUserID); err != nil {
return false, err
} else if !found {
return false, ErrContactIDInvalid
}
}
return s.contacts.Block(ctx, userID, peerUserID, date)
}
// UnblockContact removes peer from the current user's blocklist.
func (s *Service) UnblockContact(ctx context.Context, userID, peerUserID int64) (bool, error) {
if s == nil || s.contacts == nil || userID == 0 || peerUserID == 0 || peerUserID == userID {
return false, ErrContactIDInvalid
}
return s.contacts.Unblock(ctx, userID, peerUserID)
}
// IsBlocked reports whether owner has blocked peer.
func (s *Service) IsBlocked(ctx context.Context, userID, peerUserID int64) (bool, error) {
if s == nil || s.contacts == nil || userID == 0 || peerUserID == 0 {
return false, nil
}
return s.contacts.IsBlocked(ctx, userID, peerUserID)
}
// GetBlocked returns a bounded blocked contact page.
func (s *Service) GetBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error) {
if s == nil || s.contacts == nil || userID == 0 {
return domain.BlockedContactList{}, nil
}
if limit <= 0 || limit > 100 {
limit = 100
}
return s.contacts.ListBlocked(ctx, userID, offset, limit)
}
func (s *Service) ContactIDs(ctx context.Context, userID int64, hash int64) ([]int, bool, error) {
list, notModified, err := s.GetContacts(ctx, userID, hash)
if err != nil || notModified {

View file

@ -347,6 +347,8 @@ type ChannelMessage struct {
Reactions *ChannelMessageReactions
Action *ChannelMessageAction
Media *MessageMedia
Mentioned bool
MediaUnread bool
Pts int
Deleted bool
}
@ -827,6 +829,12 @@ type ChannelDifference struct {
Timeout int
}
// DirtyChannel identifies an active channel with channel-scoped updates after an account difference date.
type DirtyChannel struct {
ChannelID int64
Pts int
}
// CreateChannelRequest creates a broadcast channel or megagroup.
type CreateChannelRequest struct {
CreatorUserID int64

View file

@ -20,6 +20,18 @@ type ContactList struct {
Hash int64
}
// BlockedContact is one owner-visible blocked peer.
type BlockedContact struct {
User User
Date int
}
// BlockedContactList describes contacts.getBlocked output.
type BlockedContactList struct {
Blocked []BlockedContact
Count int
}
// ContactInput 描述一次 owner 视角联系人写入。
type ContactInput struct {
ContactUserID int64

View file

@ -73,24 +73,26 @@ type MessageEntity struct {
// Message 是账号视角下的一条私聊消息。
type Message struct {
ID int // 当前 owner 视角下的 message box id暴露给 Telegram 客户端。
UID int64 // 共享私聊消息主体 id不暴露给客户端。
RandomID int64
OwnerUserID int64
Peer Peer
From Peer
Date int
EditDate int
Out bool
Silent bool
NoForwards bool
Body string
Entities []MessageEntity
ReplyTo *MessageReply
Forward *MessageForward
Reactions *ChannelMessageReactions
Pts int
Media *MessageMedia
ID int // 当前 owner 视角下的 message box id暴露给 Telegram 客户端。
UID int64 // 共享私聊消息主体 id不暴露给客户端。
RandomID int64
OwnerUserID int64
Peer Peer
From Peer
Date int
EditDate int
Out bool
Silent bool
NoForwards bool
Body string
Entities []MessageEntity
ReplyTo *MessageReply
Forward *MessageForward
Reactions *ChannelMessageReactions
Pts int
Media *MessageMedia
MediaUnread bool
ReactionUnread bool
}
// MessageReply describes a message reply/thread header without depending on TL types.
@ -139,19 +141,20 @@ type MessageFilter struct {
// SendPrivateTextRequest 是私聊文本/媒体发送命令。
type SendPrivateTextRequest struct {
SenderUserID int64
RecipientUserID int64
RandomID int64
Message string
Entities []MessageEntity
Media *MessageMedia
Silent bool
NoForwards bool
ReplyTo *MessageReply
Forward *MessageForward
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
Message string
Entities []MessageEntity
Media *MessageMedia
Silent bool
NoForwards bool
ReplyTo *MessageReply
Forward *MessageForward
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
RecipientBlocked bool
}
// SendPrivateTextResult 描述一次私聊文本发送的双端结果。
@ -189,18 +192,19 @@ type PrivateMessageReactionsResult struct {
// ForwardPrivateMessagesRequest 是私聊文本消息转发命令。
type ForwardPrivateMessagesRequest struct {
OwnerUserID int64
FromPeer Peer
ToUserID int64
MessageIDs []int
RandomIDs []int64
Silent bool
NoForwards bool
DropAuthor bool
ReplyTo *MessageReply
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
OwnerUserID int64
FromPeer Peer
ToUserID int64
MessageIDs []int
RandomIDs []int64
Silent bool
NoForwards bool
DropAuthor bool
ReplyTo *MessageReply
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
RecipientBlocked bool
}
// ForwardPrivateMessagesResult 描述一次私聊转发的 owner 维度结果。
@ -238,14 +242,18 @@ type ReadHistoryResult struct {
// ReadMessageContentsRequest marks media/mention contents as read for exact owner-visible messages.
type ReadMessageContentsRequest struct {
OwnerUserID int64
IDs []int
OwnerUserID int64
IDs []int
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
// ReadMessageContentsResult contains owner-visible message IDs that existed and can be synced.
// ReadMessageContentsResult contains owner-visible message IDs whose content unread state changed.
type ReadMessageContentsResult struct {
OwnerUserID int64
MessageIDs []int
Event UpdateEvent
}
// OutboxReadDateRequest 是 messages.getOutboxReadDate 查询。

View file

@ -4,24 +4,25 @@ package domain
type UpdateEventType string
const (
UpdateEventNewMessage UpdateEventType = "new_message"
UpdateEventReadHistoryInbox UpdateEventType = "read_history_inbox"
UpdateEventReadHistoryOutbox UpdateEventType = "read_history_outbox"
UpdateEventEditMessage UpdateEventType = "edit_message"
UpdateEventMessageReactions UpdateEventType = "message_reactions"
UpdateEventContactsReset UpdateEventType = "contacts_reset"
UpdateEventDialogPinned UpdateEventType = "dialog_pinned"
UpdateEventPinnedDialogs UpdateEventType = "pinned_dialogs"
UpdateEventDialogUnreadMark UpdateEventType = "dialog_unread_mark"
UpdateEventPeerSettings UpdateEventType = "peer_settings"
UpdateEventDeleteMessages UpdateEventType = "delete_messages"
UpdateEventDialogFilter UpdateEventType = "dialog_filter"
UpdateEventDialogFilterOrder UpdateEventType = "dialog_filter_order"
UpdateEventDialogFilters UpdateEventType = "dialog_filters"
UpdateEventFolderPeers UpdateEventType = "folder_peers"
UpdateEventChannelAvailable UpdateEventType = "channel_available_messages"
UpdateEventChannelViewForum UpdateEventType = "channel_view_forum_as_messages"
UpdateEventNoop UpdateEventType = "noop"
UpdateEventNewMessage UpdateEventType = "new_message"
UpdateEventReadHistoryInbox UpdateEventType = "read_history_inbox"
UpdateEventReadHistoryOutbox UpdateEventType = "read_history_outbox"
UpdateEventReadMessageContents UpdateEventType = "read_message_contents"
UpdateEventEditMessage UpdateEventType = "edit_message"
UpdateEventMessageReactions UpdateEventType = "message_reactions"
UpdateEventContactsReset UpdateEventType = "contacts_reset"
UpdateEventDialogPinned UpdateEventType = "dialog_pinned"
UpdateEventPinnedDialogs UpdateEventType = "pinned_dialogs"
UpdateEventDialogUnreadMark UpdateEventType = "dialog_unread_mark"
UpdateEventPeerSettings UpdateEventType = "peer_settings"
UpdateEventDeleteMessages UpdateEventType = "delete_messages"
UpdateEventDialogFilter UpdateEventType = "dialog_filter"
UpdateEventDialogFilterOrder UpdateEventType = "dialog_filter_order"
UpdateEventDialogFilters UpdateEventType = "dialog_filters"
UpdateEventFolderPeers UpdateEventType = "folder_peers"
UpdateEventChannelAvailable UpdateEventType = "channel_available_messages"
UpdateEventChannelViewForum UpdateEventType = "channel_view_forum_as_messages"
UpdateEventNoop UpdateEventType = "noop"
)
// UpdateEvent 是账号视角的增量事件,按 user_id + pts 顺序持久化。
@ -50,9 +51,16 @@ type UpdateEvent struct {
// UpdateDifference 是 updates.getDifference 的业务层结果。
type UpdateDifference struct {
State UpdateState
Events []UpdateEvent
State UpdateState
Events []UpdateEvent
ChannelNudges []ChannelDifferenceNudge
// Partial 为 true 表示连续事件被 limit 截断、后面还有(映射 updates.differenceSlice
// 客户端据 State 继续翻页false 表示已到当前连续末尾updates.difference
Partial bool
}
// ChannelDifferenceNudge is a computed account-level hint that a channel diff is dirty.
type ChannelDifferenceNudge struct {
ChannelID int64
Pts int
}

View file

@ -2700,12 +2700,15 @@ func (r *Router) channelOperationUpdates(ctx context.Context, viewerUserID int64
for _, member := range res.Members {
users = append(users, member.UserID)
}
updates := make([]tg.UpdateClass, 0, 1)
updates := make([]tg.UpdateClass, 0, 2)
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
if res.Channel.ID != 0 {
updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID})
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, users),

View file

@ -31,6 +31,8 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
d.OnContactsImportContacts(r.onContactsImportContacts)
d.OnContactsAddContact(r.onContactsAddContact)
d.OnContactsDeleteContacts(r.onContactsDeleteContacts)
d.OnContactsBlock(r.onContactsBlock)
d.OnContactsUnblock(r.onContactsUnblock)
d.OnContactsUpdateContactNote(r.onContactsUpdateContactNote)
d.OnContactsSearch(r.onContactsSearch)
d.OnContactsResolveUsername(r.onContactsResolveUsername)
@ -38,12 +40,7 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
d.OnContactsGetTopPeers(func(ctx context.Context, req *tg.ContactsGetTopPeersRequest) (tg.ContactsTopPeersClass, error) {
return tdesktop.TopPeers(), nil
})
d.OnContactsGetBlocked(func(ctx context.Context, req *tg.ContactsGetBlockedRequest) (tg.ContactsBlockedClass, error) {
if req.Limit > 50 {
return nil, limitInvalidErr()
}
return tdesktop.BlockedContacts(), nil
})
d.OnContactsGetBlocked(r.onContactsGetBlocked)
d.OnContactsGetSponsoredPeers(func(ctx context.Context, q string) (tg.ContactsSponsoredPeersClass, error) {
if utf8.RuneCountInString(q) > maxContactSearchQLen {
return nil, limitInvalidErr()
@ -52,6 +49,81 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
})
}
func (r *Router) onContactsBlock(ctx context.Context, req *tg.ContactsBlockRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, ok := r.domainPeerFromInputPeer(userID, req.ID)
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
return false, userIDInvalidErr()
}
if r.deps.Contacts == nil {
return true, nil
}
if _, err := r.deps.Contacts.BlockContact(ctx, userID, peer.ID, int(r.clock.Now().Unix())); err != nil {
return false, contactErr(err)
}
if settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer); err == nil {
_ = r.recordPeerSettings(ctx, userID, peer, settings)
}
return true, nil
}
func (r *Router) onContactsUnblock(ctx context.Context, req *tg.ContactsUnblockRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, ok := r.domainPeerFromInputPeer(userID, req.ID)
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
return false, userIDInvalidErr()
}
if r.deps.Contacts == nil {
return true, nil
}
if _, err := r.deps.Contacts.UnblockContact(ctx, userID, peer.ID); err != nil {
return false, contactErr(err)
}
if settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer); err == nil {
_ = r.recordPeerSettings(ctx, userID, peer, settings)
}
return true, nil
}
func (r *Router) onContactsGetBlocked(ctx context.Context, req *tg.ContactsGetBlockedRequest) (tg.ContactsBlockedClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit > 100 || req.Offset < 0 {
return nil, limitInvalidErr()
}
if r.deps.Contacts == nil {
return tdesktop.BlockedContacts(), nil
}
list, err := r.deps.Contacts.GetBlocked(ctx, userID, req.Offset, req.Limit)
if err != nil {
return nil, internalErr()
}
blocked := make([]tg.PeerBlocked, 0, len(list.Blocked))
users := make([]tg.UserClass, 0, len(list.Blocked))
for _, item := range list.Blocked {
if item.User.ID == 0 {
continue
}
blocked = append(blocked, tg.PeerBlocked{
PeerID: &tg.PeerUser{UserID: item.User.ID},
Date: item.Date,
})
users = append(users, r.tgUser(item.User))
}
if list.Count > len(blocked)+req.Offset {
return &tg.ContactsBlockedSlice{Count: list.Count, Blocked: blocked, Chats: []tg.ChatClass{}, Users: users}, nil
}
return &tg.ContactsBlocked{Blocked: blocked, Chats: []tg.ChatClass{}, Users: users}, nil
}
func (r *Router) onContactsGetContacts(ctx context.Context, hash int64) (tg.ContactsContactsClass, error) {
if r.deps.Contacts == nil {
return &tg.ContactsContacts{}, nil

View file

@ -349,6 +349,16 @@ func tgUpdatesDifference(diff domain.UpdateDifference) tg.UpdatesDifferenceClass
}
}
}
for _, nudge := range diff.ChannelNudges {
if nudge.ChannelID == 0 {
continue
}
update := &tg.UpdateChannelTooLong{ChannelID: nudge.ChannelID}
if nudge.Pts > 0 {
update.SetPts(nudge.Pts)
}
out.OtherUpdates = append(out.OtherUpdates, update)
}
// Partial连续事件被 limit 截断、后面还有 → updates.differenceSlice客户端据 IntermediateState 续拉。
if diff.Partial {
return &tg.UpdatesDifferenceSlice{
@ -561,6 +571,15 @@ func tgOtherUpdateFromEvent(event domain.UpdateEvent) tg.UpdateClass {
}
case domain.UpdateEventReadHistoryOutbox:
return tgReadHistoryOutbox(event)
case domain.UpdateEventReadMessageContents:
if len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdateReadMessagesContents{
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventEditMessage:
msg := tgMessage(event.Message)
if msg == nil {
@ -856,12 +875,13 @@ func tgMessage(m domain.Message) tg.MessageClass {
return nil
}
msg := &tg.Message{
Out: m.Out,
ID: m.ID,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
Out: m.Out,
MediaUnread: m.MediaUnread,
ID: m.ID,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
}
if m.EditDate != 0 {
msg.SetEditDate(m.EditDate)
@ -993,16 +1013,18 @@ func tgChannelMessage(viewerUserID int64, m domain.ChannelMessage) tg.MessageCla
return msg
}
msg := &tg.Message{
Out: outgoing,
Silent: m.Silent,
Post: m.Post,
Noforwards: m.NoForwards,
ID: m.ID,
FromID: from,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
Out: outgoing,
Silent: m.Silent,
Post: m.Post,
Noforwards: m.NoForwards,
Mentioned: m.Mentioned,
MediaUnread: m.MediaUnread,
ID: m.ID,
FromID: from,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
}
if m.EditDate != 0 {
msg.SetEditDate(m.EditDate)

View file

@ -138,6 +138,10 @@ type ContactsService interface {
DeleteContacts(ctx context.Context, userID int64, contactUserIDs []int64) (int, error)
UpdateContactNote(ctx context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, error)
GetPeerSettings(ctx context.Context, userID int64, peer domain.Peer) (domain.PeerSettings, error)
BlockContact(ctx context.Context, userID, peerUserID int64, date int) (bool, error)
UnblockContact(ctx context.Context, userID, peerUserID int64) (bool, error)
IsBlocked(ctx context.Context, userID, peerUserID int64) (bool, error)
GetBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error)
}
// DialogsService 抽象会话列表查询。
@ -278,6 +282,7 @@ type ChannelsService interface {
GetMessageReadParticipants(ctx context.Context, userID int64, req domain.ChannelReadParticipantsRequest) (domain.ChannelReadParticipantsResult, error)
GetDifference(ctx context.Context, userID int64, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error)
ActiveChannelIDsForUser(ctx context.Context, userID, afterChannelID int64, limit int) ([]int64, error)
DirtyActiveChannelsForUser(ctx context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error)
ActiveMemberIDs(ctx context.Context, userID, channelID int64, limit int) ([]int64, error)
InviteAdminMemberIDs(ctx context.Context, channelID int64, limit int) ([]int64, error)
FilterActiveMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)

View file

@ -140,6 +140,10 @@ func messageAuthorRequiredErr() error { return tgerr.New(403, "MESSAGE_AUTHOR_RE
func messageNotModifiedErr() error { return tgerr.New(400, "MESSAGE_NOT_MODIFIED") }
func messageEditForbiddenErr() error { return tgerr.New(403, "EDIT_MESSAGES_FORBIDDEN") }
func messageDeleteForbiddenErr() error { return tgerr.New(403, "DELETE_MESSAGES_FORBIDDEN") }
func messageNotReadYetErr() error { return tgerr.New(400, "MESSAGE_NOT_READ_YET") }
func replyMessageIDInvalidErr() error { return tgerr.New(400, "REPLY_MESSAGE_ID_INVALID") }

View file

@ -2808,6 +2808,7 @@ func (r *Router) onMessagesGetSponsoredMessages(ctx context.Context, req *tg.Mes
func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (*tg.MessagesAffectedMessages, error) {
id, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
@ -2823,8 +2824,11 @@ func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (
read := domain.ReadMessageContentsResult{OwnerUserID: userID}
if r.deps.Messages != nil {
read, err = r.deps.Messages.ReadMessageContents(ctx, userID, domain.ReadMessageContentsRequest{
OwnerUserID: userID,
IDs: ids,
OwnerUserID: userID,
IDs: ids,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: id,
OriginSessionID: sessionID,
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
@ -2833,12 +2837,15 @@ func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (
return nil, internalErr()
}
}
affected, err := r.affectedMessages(ctx, id, userID)
if err != nil {
return nil, err
affected := &tg.MessagesAffectedMessages{Pts: read.Event.Pts, PtsCount: read.Event.PtsCount}
if read.Event.Pts == 0 {
affected, err = r.affectedMessages(ctx, id, userID)
if err != nil {
return nil, err
}
}
if contentIDs := readMessageContentIDs(read.MessageIDs); len(contentIDs) > 0 {
r.pushUserUpdates(ctx, userID, &tg.Updates{
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateReadMessagesContents{
Messages: contentIDs,
Pts: affected.Pts,
@ -4378,6 +4385,10 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
if r.deps.Channels == nil || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
sources, err := r.forwardSources(ctx, userID, fromPeer, req.ID)
if err != nil {
return nil, messageForwardErr(err)
@ -4391,19 +4402,20 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
forward = nil
}
sent, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
SenderUserID: userID,
RecipientUserID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
@ -4421,19 +4433,24 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
res, err := r.deps.Messages.ForwardPrivateMessages(ctx, userID, domain.ForwardPrivateMessagesRequest{
OwnerUserID: userID,
FromPeer: fromPeer,
ToUserID: toPeer.ID,
MessageIDs: append([]int(nil), req.ID...),
RandomIDs: append([]int64(nil), req.RandomID...),
Silent: req.Silent,
NoForwards: req.Noforwards,
DropAuthor: req.DropAuthor,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
OwnerUserID: userID,
FromPeer: fromPeer,
ToUserID: toPeer.ID,
MessageIDs: append([]int(nil), req.ID...),
RandomIDs: append([]int64(nil), req.RandomID...),
Silent: req.Silent,
NoForwards: req.Noforwards,
DropAuthor: req.DropAuthor,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
@ -4641,6 +4658,13 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit
if peer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageEditForbiddenErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.EditMessage(ctx, userID, domain.EditMessageRequest{
@ -4818,6 +4842,19 @@ func (r *Router) onMessagesDeleteMessages(ctx context.Context, req *tg.MessagesD
if len(req.ID) > domain.MaxDeleteMessageIDs {
return nil, limitInvalidErr()
}
if req.GetRevoke() {
list, err := r.deps.Messages.GetMessages(ctx, userID, req.ID)
if err != nil {
return nil, internalErr()
}
blocked, err := r.messagesTouchBlockedPeer(ctx, userID, list.Messages)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteMessages(ctx, userID, domain.DeleteMessagesRequest{
OwnerUserID: userID,
@ -4892,6 +4929,15 @@ func (r *Router) onMessagesDeleteHistory(ctx context.Context, req *tg.MessagesDe
if r.deps.Messages == nil {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
if req.GetRevoke() {
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteHistory(ctx, userID, domain.DeleteHistoryRequest{
OwnerUserID: userID,
@ -4972,6 +5018,38 @@ func messageSendErr(err error) error {
}
}
func (r *Router) peerBlocksUser(ctx context.Context, userID, peerUserID int64) (bool, error) {
if userID == 0 || peerUserID == 0 || userID == peerUserID || r.deps.Contacts == nil {
return false, nil
}
blocked, err := r.deps.Contacts.IsBlocked(ctx, peerUserID, userID)
if err != nil {
return false, internalErr()
}
return blocked, nil
}
func (r *Router) messagesTouchBlockedPeer(ctx context.Context, userID int64, messages []domain.Message) (bool, error) {
seen := make(map[int64]struct{}, len(messages))
for _, msg := range messages {
if msg.Peer.Type != domain.PeerTypeUser || msg.Peer.ID == 0 {
continue
}
if _, ok := seen[msg.Peer.ID]; ok {
continue
}
seen[msg.Peer.ID] = struct{}{}
blocked, err := r.peerBlocksUser(ctx, userID, msg.Peer.ID)
if err != nil {
return false, err
}
if blocked {
return true, nil
}
}
return false, nil
}
func messageForwardErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):

View file

@ -829,13 +829,16 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) {
if !ok || !channel.Megagroup || channel.Broadcast {
t.Fatalf("chat = %#v, want megagroup channel", updates.Chats[0])
}
if len(updates.Updates) != 2 {
t.Fatalf("updates len = %d, want create + invite service messages", len(updates.Updates))
if len(updates.Updates) != 4 {
t.Fatalf("updates len = %d, want create/invite service messages plus channel refreshes", len(updates.Updates))
}
newMsg, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage)
if !ok || newMsg.Pts != 1 || newMsg.PtsCount != 1 {
t.Fatalf("create update = %#v, want channel pts=1", updates.Updates[0])
}
if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("create refresh = %#v, want channel refresh", updates.Updates[1])
}
service, ok := newMsg.Message.(*tg.MessageService)
if !ok {
t.Fatalf("create message = %T, want service", newMsg.Message)
@ -843,9 +846,12 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) {
if _, ok := service.Action.(*tg.MessageActionChannelCreate); !ok {
t.Fatalf("service action = %T, want channel create", service.Action)
}
inviteMsg, ok := updates.Updates[1].(*tg.UpdateNewChannelMessage)
inviteMsg, ok := updates.Updates[2].(*tg.UpdateNewChannelMessage)
if !ok || inviteMsg.Pts != 2 || inviteMsg.PtsCount != 1 {
t.Fatalf("invite update = %#v, want channel pts=2", updates.Updates[1])
t.Fatalf("invite update = %#v, want channel pts=2", updates.Updates[2])
}
if refresh, ok := updates.Updates[3].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("invite refresh = %#v, want channel refresh", updates.Updates[3])
}
inviteService, ok := inviteMsg.Message.(*tg.MessageService)
if !ok {
@ -1102,8 +1108,8 @@ func TestMessagesCreateChatDispatchRemembersTDesktopClientInfo(t *testing.T) {
if !ok || !channel.Megagroup || !channel.Creator {
t.Fatalf("second chat = %#v, want creator megagroup channel", updates.Chats[1])
}
if len(updates.Updates) != 2 {
t.Fatalf("updates len = %d, want create + invite service messages", len(updates.Updates))
if len(updates.Updates) != 4 {
t.Fatalf("updates len = %d, want create/invite service messages plus channel refreshes", len(updates.Updates))
}
participants, err := r.onChannelsGetParticipants(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantsRequest{
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
@ -4337,8 +4343,12 @@ func TestChannelAdminPinInviteRPC(t *testing.T) {
if err != nil {
t.Fatalf("import invite: %v", err)
}
if updates := imported.(*tg.Updates); len(updates.Chats) != 1 || len(updates.Updates) != 1 {
t.Fatalf("import updates = %+v, want chat and join service update", updates)
if updates := imported.(*tg.Updates); len(updates.Chats) != 1 || len(updates.Updates) != 2 {
t.Fatalf("import updates = %+v, want chat, join service update, and channel refresh", updates)
} else if _, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage); !ok {
t.Fatalf("import first update = %T, want join service update", updates.Updates[0])
} else if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("import second update = %#v, want channel refresh", updates.Updates[1])
}
inviteList, err := r.onMessagesGetExportedChatInvites(WithUserID(ctx, friend.ID), &tg.MessagesGetExportedChatInvitesRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
@ -7202,6 +7212,29 @@ func TestUpdatesDifferenceIncludesDeleteMessages(t *testing.T) {
}
}
func TestUpdatesDifferenceIncludesChannelTooLongNudge(t *testing.T) {
got, ok := tgUpdatesDifference(domain.UpdateDifference{
State: domain.UpdateState{Pts: 8, Date: 1700000250, Seq: 0},
ChannelNudges: []domain.ChannelDifferenceNudge{{
ChannelID: 2000000001,
Pts: 12,
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if got.State.Pts != 8 || len(got.OtherUpdates) != 1 {
t.Fatalf("difference = %+v, want one channel nudge and account pts unchanged", got)
}
update, ok := got.OtherUpdates[0].(*tg.UpdateChannelTooLong)
if !ok || update.ChannelID != 2000000001 {
t.Fatalf("update = %T %+v, want UpdateChannelTooLong", got.OtherUpdates[0], got.OtherUpdates[0])
}
if pts, ok := update.GetPts(); !ok || pts != 12 {
t.Fatalf("channel nudge pts = %d set=%v, want 12", pts, ok)
}
}
func TestUpdatesDifferenceIncludesSettingsUpdates(t *testing.T) {
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
got, ok := tgUpdatesDifference(domain.UpdateDifference{
@ -8354,6 +8387,137 @@ func TestMessagesSendMessageReturnsUpdateAndRecordsOwnerContext(t *testing.T) {
}
}
func TestContactsBlockGetBlockedAndUnblockRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009001", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009002", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Contacts: appcontacts.NewService(memory.NewContactStore(), userStore),
}, zaptest.NewLogger(t), clock.System)
ok, err := r.onContactsBlock(WithUserID(ctx, bob.ID), &tg.ContactsBlockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
})
if err != nil || !ok {
t.Fatalf("contacts.block = %v, %v", ok, err)
}
blocked, err := r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
if err != nil {
t.Fatalf("contacts.getBlocked: %v", err)
}
full, ok := blocked.(*tg.ContactsBlocked)
if !ok || len(full.Blocked) != 1 || len(full.Users) != 1 {
t.Fatalf("blocked = %T %+v, want one blocked user", blocked, blocked)
}
if peer, ok := full.Blocked[0].PeerID.(*tg.PeerUser); !ok || peer.UserID != alice.ID {
t.Fatalf("blocked peer = %#v, want alice", full.Blocked[0].PeerID)
}
if user, ok := full.Users[0].(*tg.User); !ok || user.ID != alice.ID {
t.Fatalf("blocked user = %#v, want alice", full.Users[0])
}
ok, err = r.onContactsUnblock(WithUserID(ctx, bob.ID), &tg.ContactsUnblockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
})
if err != nil || !ok {
t.Fatalf("contacts.unblock = %v, %v", ok, err)
}
blocked, err = r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
if err != nil {
t.Fatalf("contacts.getBlocked after unblock: %v", err)
}
if full, ok := blocked.(*tg.ContactsBlocked); !ok || len(full.Blocked) != 0 {
t.Fatalf("blocked after unblock = %T %+v, want empty contacts.blocked", blocked, blocked)
}
}
func TestMessagesPrivateBlockPreventsRecipientInboxAndRevokeRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009101", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009102", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
dialogs := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogs)
contactStore := memory.NewContactStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Contacts: appcontacts.NewService(contactStore, userStore),
Messages: appmessages.NewService(messageStore, dialogs),
Dialogs: appdialogs.NewService(dialogs),
}, zaptest.NewLogger(t), clock.System)
delivered, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "before block",
RandomID: 91001,
})
if err != nil {
t.Fatalf("send before block: %v", err)
}
deliveredUpdates := delivered.(*tg.Updates)
deliveredMsg := deliveredUpdates.Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if ok, err := r.onContactsBlock(WithUserID(ctx, bob.ID), &tg.ContactsBlockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
}); err != nil || !ok {
t.Fatalf("bob block alice = %v, %v", ok, err)
}
blockedSend, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "after block",
RandomID: 91002,
})
if err != nil {
t.Fatalf("send after block: %v", err)
}
blockedUpdates := blockedSend.(*tg.Updates)
blockedMsg := blockedUpdates.Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if blockedMsg.ID == 0 || !blockedMsg.Out {
t.Fatalf("blocked sender update = %#v, want outgoing sender message", blockedMsg)
}
bobHistory, err := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID},
Limit: 10,
})
if err != nil {
t.Fatalf("bob history: %v", err)
}
if len(bobHistory.Messages) != 1 || bobHistory.Messages[0].Body != "before block" {
t.Fatalf("bob history = %+v, want only pre-block delivered message", bobHistory.Messages)
}
aliceHistory, err := messageStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID},
Limit: 10,
})
if err != nil {
t.Fatalf("alice history: %v", err)
}
if len(aliceHistory.Messages) != 2 {
t.Fatalf("alice history len = %d, want delivered + sender-only blocked message", len(aliceHistory.Messages))
}
deleteReq := &tg.MessagesDeleteMessagesRequest{ID: []int{deliveredMsg.ID}}
deleteReq.SetRevoke(true)
if _, err := r.onMessagesDeleteMessages(WithUserID(ctx, alice.ID), deleteReq); err == nil || !strings.Contains(err.Error(), "DELETE_MESSAGES_FORBIDDEN") {
t.Fatalf("revoke after block err = %v, want DELETE_MESSAGES_FORBIDDEN", err)
}
}
func TestMessagesSendMessageSupportsReplyAndFlags(t *testing.T) {
const (
senderID = int64(1000000001)

View file

@ -94,21 +94,26 @@ func (r *Router) sendOutgoing(ctx context.Context, userID int64, peer domain.Pee
if err != nil {
return nil, false, err
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, false, err
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: peer.ID,
RandomID: p.randomID,
Message: p.message,
Entities: domainMessageEntities(p.entities),
Media: p.media,
Silent: p.silent,
NoForwards: p.noforwards,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
SenderUserID: userID,
RecipientUserID: peer.ID,
RandomID: p.randomID,
Message: p.message,
Entities: domainMessageEntities(p.entities),
Media: p.media,
Silent: p.silent,
NoForwards: p.noforwards,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, false, messageSendErr(err)

View file

@ -63,13 +63,32 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
return nil, internalErr()
}
r.markSessionReceivesUpdates(ctx, userID)
if len(st.Events) == 0 {
st.ChannelNudges = r.accountChannelDifferenceNudges(ctx, userID, req.Date)
if len(st.Events) == 0 && len(st.ChannelNudges) == 0 {
return &tg.UpdatesDifferenceEmpty{Date: st.State.Date, Seq: st.State.Seq}, nil
}
st.Events = r.enrichUpdateEvents(ctx, userID, st.Events)
return tgUpdatesDifference(st), nil
}
func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int64, sinceDate int) []domain.ChannelDifferenceNudge {
if r.deps.Channels == nil || userID == 0 || sinceDate <= 0 {
return nil
}
dirty, err := r.deps.Channels.DirtyActiveChannelsForUser(ctx, userID, sinceDate, 0, domain.MaxChannelDifferenceLimit)
if err != nil || len(dirty) == 0 {
return nil
}
out := make([]domain.ChannelDifferenceNudge, 0, len(dirty))
for _, item := range dirty {
if item.ChannelID == 0 {
continue
}
out = append(out, domain.ChannelDifferenceNudge{ChannelID: item.ChannelID, Pts: item.Pts})
}
return out
}
func (r *Router) markSessionReceivesUpdates(ctx context.Context, userID int64) {
if r.deps.Sessions == nil {
return

View file

@ -109,6 +109,7 @@ type ChannelStore interface {
ListMessageReadParticipants(ctx context.Context, req domain.ChannelReadParticipantsRequest) (domain.ChannelReadParticipantsResult, error)
ListChannelDifference(ctx context.Context, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error)
ListActiveChannelIDsForUser(ctx context.Context, userID, afterChannelID int64, limit int) ([]int64, error)
ListDirtyActiveChannelsForUser(ctx context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error)
ListActiveChannelMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error)
ListChannelInviteAdminMemberIDs(ctx context.Context, channelID int64, limit int) ([]int64, error)
FilterActiveChannelMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)

View file

@ -14,4 +14,8 @@ type ContactStore interface {
UpsertMany(ctx context.Context, userID int64, inputs []domain.ContactInput) ([]domain.Contact, error)
UpdateNote(ctx context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, bool, error)
Delete(ctx context.Context, userID int64, contactUserIDs []int64) (int, error)
Block(ctx context.Context, userID, blockedUserID int64, date int) (bool, error)
Unblock(ctx context.Context, userID, blockedUserID int64) (bool, error)
IsBlocked(ctx context.Context, userID, blockedUserID int64) (bool, error)
ListBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error)
}

View file

@ -4937,6 +4937,7 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann
}
messages = append(messages, cloneChannelMessage(msg))
}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, messages)
return domain.ChannelDifference{
Channel: channel,
Self: member,
@ -4991,6 +4992,15 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann
diff.OtherUpdates = append(diff.OtherUpdates, cloneChannelEvent(event))
}
}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, diff.NewMessages)
for i := range diff.OtherUpdates {
if diff.OtherUpdates[i].Message.ID == 0 {
continue
}
messages := []domain.ChannelMessage{diff.OtherUpdates[i].Message}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, messages)
diff.OtherUpdates[i].Message = messages[0]
}
return diff, nil
}
@ -5025,6 +5035,46 @@ func (s *ChannelStore) ListActiveChannelIDsForUser(_ context.Context, userID, af
return out, nil
}
func (s *ChannelStore) ListDirtyActiveChannelsForUser(_ context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if userID == 0 || sinceDate <= 0 || afterChannelID < 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
out := make([]domain.DirtyChannel, 0, limit)
for channelID, members := range s.members {
if channelID <= afterChannelID {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
continue
}
member, ok := members[userID]
if !ok || member.Status != domain.ChannelMemberActive {
continue
}
dirty := false
for _, event := range s.events[channelID] {
if event.Date > sinceDate {
dirty = true
break
}
}
if dirty {
out = append(out, domain.DirtyChannel{ChannelID: channelID, Pts: channel.Pts})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].ChannelID < out[j].ChannelID })
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (s *ChannelStore) ListActiveChannelMemberIDs(_ context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@ -6495,6 +6545,7 @@ func (s *ChannelStore) populateChannelMessageReactionsLocked(viewerUserID int64,
if len(messages) == 0 || channel.ID == 0 {
return
}
s.populateChannelMessageUnreadFlagsLocked(viewerUserID, messages)
for i := range messages {
if messages[i].ChannelID != channel.ID || messages[i].ID <= 0 {
continue
@ -6511,6 +6562,7 @@ func (s *ChannelStore) populateChannelMessagesReactionsLocked(viewerUserID int64
if len(messages) == 0 {
return
}
s.populateChannelMessageUnreadFlagsLocked(viewerUserID, messages)
channelsByID := make(map[int64]domain.Channel, len(channels))
for _, ch := range channels {
if ch.ID != 0 {
@ -6533,6 +6585,22 @@ func (s *ChannelStore) populateChannelMessagesReactionsLocked(viewerUserID int64
}
}
func (s *ChannelStore) populateChannelMessageUnreadFlagsLocked(viewerUserID int64, messages []domain.ChannelMessage) {
if viewerUserID == 0 || len(messages) == 0 {
return
}
for i := range messages {
if messages[i].ChannelID == 0 || messages[i].ID <= 0 {
continue
}
if _, ok := s.mentions[viewerUserID][messages[i].ChannelID][messages[i].ID]; !ok {
continue
}
messages[i].Mentioned = true
messages[i].MediaUnread = !messages[i].Media.IsZero()
}
}
type memoryReactionCursor struct {
date int
userID int64

View file

@ -242,13 +242,17 @@ func (s *TempAuthKeyBindingStore) GetByTemp(_ context.Context, tempAuthKeyID [8]
// ContactStore 是 store.ContactStore 的内存实现。
type ContactStore struct {
mu sync.RWMutex
m map[int64]domain.ContactList
mu sync.RWMutex
m map[int64]domain.ContactList
blocks map[int64]map[int64]domain.BlockedContact
}
// NewContactStore 创建内存 ContactStore。
func NewContactStore() *ContactStore {
return &ContactStore{m: make(map[int64]domain.ContactList)}
return &ContactStore{
m: make(map[int64]domain.ContactList),
blocks: make(map[int64]map[int64]domain.BlockedContact),
}
}
func (s *ContactStore) ListByUser(_ context.Context, userID int64) (domain.ContactList, error) {
@ -400,6 +404,68 @@ func (s *ContactStore) Delete(_ context.Context, userID int64, contactUserIDs []
return deleted, nil
}
func (s *ContactStore) Block(_ context.Context, userID, blockedUserID int64, date int) (bool, error) {
if userID == 0 || blockedUserID == 0 || userID == blockedUserID {
return false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.blocks[userID] == nil {
s.blocks[userID] = make(map[int64]domain.BlockedContact)
}
_, existed := s.blocks[userID][blockedUserID]
s.blocks[userID][blockedUserID] = domain.BlockedContact{
User: domain.User{ID: blockedUserID},
Date: date,
}
return !existed, nil
}
func (s *ContactStore) Unblock(_ context.Context, userID, blockedUserID int64) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.blocks[userID] == nil {
return false, nil
}
_, existed := s.blocks[userID][blockedUserID]
delete(s.blocks[userID], blockedUserID)
return existed, nil
}
func (s *ContactStore) IsBlocked(_ context.Context, userID, blockedUserID int64) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
_, blocked := s.blocks[userID][blockedUserID]
return blocked, nil
}
func (s *ContactStore) ListBlocked(_ context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error) {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]domain.BlockedContact, 0, len(s.blocks[userID]))
for _, item := range s.blocks[userID] {
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Date == items[j].Date {
return items[i].User.ID > items[j].User.ID
}
return items[i].Date > items[j].Date
})
total := len(items)
if offset < 0 {
offset = 0
}
if offset >= len(items) {
return domain.BlockedContactList{Count: total}, nil
}
if limit <= 0 || limit > len(items)-offset {
limit = len(items) - offset
}
out := append([]domain.BlockedContact(nil), items[offset:offset+limit]...)
return domain.BlockedContactList{Blocked: out, Count: total}, nil
}
// SaveList 保存一份用户通讯录,供测试和本地替身使用。
func (s *ContactStore) SaveList(_ context.Context, userID int64, list domain.ContactList) error {
list.Contacts = cloneContacts(list.Contacts)
@ -956,21 +1022,30 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
Forward: cloneMessageForward(req.Forward),
Pts: s.nextPtsLocked(req.SenderUserID),
}
recipient := sender
if req.SenderUserID != req.RecipientUserID {
recipient := domain.Message{}
if req.SenderUserID == req.RecipientUserID {
recipient = sender
}
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
recipient = sender
recipient.ID = s.nextBoxIDLocked(req.RecipientUserID)
recipient.OwnerUserID = req.RecipientUserID
recipient.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
recipient.Out = false
recipient.ReplyTo = cloneMessageReply(recipientReply)
recipient.Pts = s.nextPtsLocked(req.RecipientUserID)
recipient.MediaUnread = !req.Media.IsZero()
}
s.m[req.SenderUserID] = append(s.m[req.SenderUserID], sender)
if req.SenderUserID != req.RecipientUserID {
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
s.m[req.RecipientUserID] = append(s.m[req.RecipientUserID], recipient)
}
if s.dialogs != nil {
s.upsertMemoryDialogsLocked(sender, recipient)
if recipient.ID != 0 {
s.upsertMemoryDialogsLocked(sender, recipient)
} else {
s.upsertMemoryDialogsLocked(sender, sender)
}
}
return domain.SendPrivateTextResult{
SenderMessage: cloneMessage(sender),
@ -1078,18 +1153,19 @@ func (s *MessageStore) ForwardPrivateMessages(ctx context.Context, req domain.Fo
}
}
sent, err := s.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: req.OwnerUserID,
RecipientUserID: req.ToUserID,
RandomID: req.RandomIDs[i],
Message: source.Body,
Entities: append([]domain.MessageEntity(nil), source.Entities...),
Silent: req.Silent,
NoForwards: req.NoForwards,
ReplyTo: req.ReplyTo,
Forward: forward,
Date: req.Date,
OriginAuthKeyID: req.OriginAuthKeyID,
OriginSessionID: req.OriginSessionID,
SenderUserID: req.OwnerUserID,
RecipientUserID: req.ToUserID,
RandomID: req.RandomIDs[i],
Message: source.Body,
Entities: append([]domain.MessageEntity(nil), source.Entities...),
Silent: req.Silent,
NoForwards: req.NoForwards,
ReplyTo: req.ReplyTo,
Forward: forward,
Date: req.Date,
OriginAuthKeyID: req.OriginAuthKeyID,
OriginSessionID: req.OriginSessionID,
RecipientBlocked: req.RecipientBlocked,
})
if err != nil {
return res, err
@ -1277,14 +1353,52 @@ func (s *MessageStore) ReadMessageContents(_ context.Context, req domain.ReadMes
if len(wanted) == 0 {
return res, nil
}
s.mu.RLock()
for _, msg := range s.m[req.OwnerUserID] {
if _, ok := wanted[msg.ID]; ok {
res.MessageIDs = append(res.MessageIDs, msg.ID)
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
affectedPeers := make(map[domain.Peer]struct{})
for i := range s.m[req.OwnerUserID] {
msg := &s.m[req.OwnerUserID][i]
if _, ok := wanted[msg.ID]; !ok {
continue
}
if !msg.MediaUnread && !msg.ReactionUnread {
continue
}
if msg.ReactionUnread && msg.Peer.ID != 0 {
affectedPeers[msg.Peer] = struct{}{}
}
msg.MediaUnread = false
msg.ReactionUnread = false
res.MessageIDs = append(res.MessageIDs, msg.ID)
}
s.mu.RUnlock()
sort.Ints(res.MessageIDs)
if len(res.MessageIDs) == 0 {
return res, nil
}
if s.dialogs != nil && len(affectedPeers) > 0 {
s.dialogs.mu.Lock()
list := s.dialogs.m[req.OwnerUserID]
for i := range list.Dialogs {
if _, ok := affectedPeers[list.Dialogs[i].Peer]; !ok {
continue
}
list.Dialogs[i].UnreadReactions = s.countPrivateUnreadReactionsLocked(req.OwnerUserID, list.Dialogs[i].Peer)
}
s.dialogs.m[req.OwnerUserID] = list
s.dialogs.mu.Unlock()
}
pts := s.nextPtsNLocked(req.OwnerUserID, len(res.MessageIDs))
res.Event = domain.UpdateEvent{
UserID: req.OwnerUserID,
Type: domain.UpdateEventReadMessageContents,
Pts: pts,
PtsCount: len(res.MessageIDs),
Date: req.Date,
MessageIDs: append([]int(nil), res.MessageIDs...),
}
return res, nil
}
@ -1355,6 +1469,27 @@ func (s *MessageStore) SetMessageReactions(_ context.Context, req domain.SetPriv
} else {
s.privateReactions[target.UID][req.UserID] = rows
}
if target.From.ID != 0 && target.From.ID != req.UserID {
for i := range s.m[target.From.ID] {
if s.m[target.From.ID][i].UID != target.UID {
continue
}
s.m[target.From.ID][i].ReactionUnread = len(rows) > 0
if s.dialogs != nil {
s.dialogs.mu.Lock()
list := s.dialogs.m[target.From.ID]
peer := domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}
for j := range list.Dialogs {
if list.Dialogs[j].Peer == peer {
list.Dialogs[j].UnreadReactions = s.countPrivateUnreadReactionsLocked(target.From.ID, peer)
}
}
s.dialogs.m[target.From.ID] = list
s.dialogs.mu.Unlock()
}
break
}
}
return s.privateReactionResultLocked(target.UID), nil
}
@ -1585,6 +1720,16 @@ func (s *MessageStore) nextPtsNLocked(userID int64, count int) int {
return next
}
func (s *MessageStore) countPrivateUnreadReactionsLocked(ownerUserID int64, peer domain.Peer) int {
count := 0
for _, msg := range s.m[ownerUserID] {
if msg.Peer == peer && msg.ReactionUnread {
count++
}
}
return count
}
func (s *MessageStore) upsertMemoryDialogsLocked(sender, recipient domain.Message) {
s.dialogs.mu.Lock()
defer s.dialogs.mu.Unlock()

View file

@ -120,6 +120,31 @@ func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing
if got := aliceReactions.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || !got[0].Big || got[0].My {
t.Fatalf("alice recent reactions = %+v, want bob non-my big reaction", got)
}
aliceBox, err := messages.GetByIDs(ctx, aliceID, []int{sent.SenderMessage.ID})
if err != nil {
t.Fatalf("alice GetByIDs after reaction: %v", err)
}
if len(aliceBox.Messages) != 1 || !aliceBox.Messages[0].ReactionUnread {
t.Fatalf("alice box after reaction = %+v, want reaction_unread", aliceBox.Messages)
}
read, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
OwnerUserID: aliceID,
IDs: []int{sent.SenderMessage.ID},
Date: 1700000210,
})
if err != nil {
t.Fatalf("ReadMessageContents reaction: %v", err)
}
if !reflect.DeepEqual(read.MessageIDs, []int{sent.SenderMessage.ID}) || read.Event.Type != domain.UpdateEventReadMessageContents || read.Event.Pts == 0 {
t.Fatalf("read reaction contents = %+v, want one read_message_contents event", read)
}
aliceBox, err = messages.GetByIDs(ctx, aliceID, []int{sent.SenderMessage.ID})
if err != nil {
t.Fatalf("alice GetByIDs after read reaction: %v", err)
}
if len(aliceBox.Messages) != 1 || aliceBox.Messages[0].ReactionUnread {
t.Fatalf("alice box after read reaction = %+v, want reaction_unread cleared", aliceBox.Messages)
}
bobReactions, err := messages.GetMessageReactions(ctx, domain.PrivateMessageReactionsRequest{
OwnerUserID: bobID,
@ -344,7 +369,7 @@ func TestMessageStoreReadHistoryEmitsInboxAndOutboxReceipts(t *testing.T) {
}
}
func TestMessageStoreReadMessageContentsReturnsExistingOwnerIDs(t *testing.T) {
func TestMessageStoreReadMessageContentsClearsUnreadContentOnce(t *testing.T) {
ctx := context.Background()
messages := NewMessageStore()
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
@ -352,20 +377,39 @@ func TestMessageStoreReadMessageContentsReturnsExistingOwnerIDs(t *testing.T) {
RecipientUserID: 1002,
RandomID: 88,
Message: "voice placeholder",
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Voice: true},
Date: 1700000300,
})
if err != nil {
t.Fatalf("SendPrivateText: %v", err)
}
if !sent.RecipientMessage.MediaUnread {
t.Fatalf("recipient MediaUnread = false, want true for incoming media")
}
got, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
OwnerUserID: 1002,
IDs: []int{sent.RecipientMessage.ID, domain.MaxMessageBoxID},
Date: 1700000400,
})
if err != nil {
t.Fatalf("ReadMessageContents: %v", err)
}
if !reflect.DeepEqual(got.MessageIDs, []int{sent.RecipientMessage.ID}) {
t.Fatalf("MessageIDs = %v, want existing recipient id", got.MessageIDs)
t.Fatalf("MessageIDs = %v, want unread recipient id", got.MessageIDs)
}
if got.Event.Type != domain.UpdateEventReadMessageContents || got.Event.Pts == 0 || got.Event.PtsCount != 1 {
t.Fatalf("Event = %+v, want read_message_contents pts update", got.Event)
}
repeated, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
OwnerUserID: 1002,
IDs: []int{sent.RecipientMessage.ID},
Date: 1700000500,
})
if err != nil {
t.Fatalf("ReadMessageContents repeat: %v", err)
}
if len(repeated.MessageIDs) != 0 || repeated.Event.Pts != 0 {
t.Fatalf("repeat = %+v, want no affected messages and no pts", repeated)
}
if _, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
OwnerUserID: 1002,

View file

@ -6570,6 +6570,9 @@ LIMIT $`+fmt.Sprint(len(args)), args...)
if err := rows.Err(); err != nil {
return domain.ChannelDifference{}, err
}
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, diff.NewMessages); err != nil {
return domain.ChannelDifference{}, err
}
if preview {
diff.Dialog = previewChannelDialog(req.UserID, channel, member)
} else {
@ -6634,6 +6637,19 @@ LIMIT $3`, req.ChannelID, req.Pts, limit)
} else if lastPts > diff.Pts {
diff.Pts = lastPts
}
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, diff.NewMessages); err != nil {
return domain.ChannelDifference{}, err
}
for i := range diff.OtherUpdates {
if diff.OtherUpdates[i].Message.ID == 0 {
continue
}
messages := []domain.ChannelMessage{diff.OtherUpdates[i].Message}
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, messages); err != nil {
return domain.ChannelDifference{}, err
}
diff.OtherUpdates[i].Message = messages[0]
}
users, err := listUsersByIDs(ctx, s.db, mapKeysInt64(userRefs))
if err != nil {
return domain.ChannelDifference{}, err
@ -6687,6 +6703,45 @@ LIMIT $3`, userID, afterChannelID, limit)
return out, rows.Err()
}
func (s *ChannelStore) ListDirtyActiveChannelsForUser(ctx context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error) {
if userID == 0 || sinceDate <= 0 || afterChannelID < 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
rows, err := s.db.Query(ctx, `
SELECT i.channel_id, c.pts
FROM user_channel_member_index i
JOIN channels c ON c.id = i.channel_id AND NOT c.deleted
WHERE i.user_id = $1
AND i.status = 'active'
AND NOT i.deleted
AND i.channel_id > $3
AND EXISTS (
SELECT 1
FROM channel_update_events e
WHERE e.channel_id = i.channel_id
AND e.date > $2
LIMIT 1
)
ORDER BY i.channel_id ASC
LIMIT $4`, userID, sinceDate, afterChannelID, limit)
if err != nil {
return nil, fmt.Errorf("list dirty active channels for user: %w", err)
}
defer rows.Close()
out := make([]domain.DirtyChannel, 0, limit)
for rows.Next() {
var item domain.DirtyChannel
if err := rows.Scan(&item.ChannelID, &item.Pts); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (s *ChannelStore) ListActiveChannelMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
if _, _, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID); err != nil {
return nil, err
@ -7119,6 +7174,9 @@ func (s *ChannelStore) populateChannelMessagesReactions(ctx context.Context, db
if len(messages) == 0 {
return nil
}
if err := populateChannelMessageUnreadFlags(ctx, db, viewerUserID, messages); err != nil {
return err
}
channelsByID := make(map[int64]domain.Channel, len(channels))
for _, ch := range channels {
if ch.ID != 0 {
@ -7230,6 +7288,54 @@ ORDER BY message_id ASC, reaction_date DESC, reacted_user_id DESC, reaction_valu
return nil
}
func populateChannelMessageUnreadFlags(ctx context.Context, db sqlcgen.DBTX, viewerUserID int64, messages []domain.ChannelMessage) error {
if viewerUserID == 0 || len(messages) == 0 {
return nil
}
indexes := make(map[channelReactionMessageKey][]int)
idsByChannel := make(map[int64][]int32)
for i := range messages {
if messages[i].ChannelID == 0 || messages[i].ID <= 0 {
continue
}
key := channelReactionMessageKey{channelID: messages[i].ChannelID, messageID: messages[i].ID}
if _, ok := indexes[key]; !ok {
idsByChannel[messages[i].ChannelID] = append(idsByChannel[messages[i].ChannelID], int32(messages[i].ID))
}
indexes[key] = append(indexes[key], i)
}
for channelID, ids := range idsByChannel {
rows, err := db.Query(ctx, `
SELECT message_id, COALESCE(media_unread, false)
FROM channel_unread_mentions
WHERE user_id = $1
AND channel_id = $2
AND message_id = ANY($3::int[])`, viewerUserID, channelID, ids)
if err != nil {
return fmt.Errorf("load channel message unread flags: %w", err)
}
for rows.Next() {
var messageID int
var mediaUnread bool
if err := rows.Scan(&messageID, &mediaUnread); err != nil {
rows.Close()
return err
}
key := channelReactionMessageKey{channelID: channelID, messageID: messageID}
for _, idx := range indexes[key] {
messages[idx].Mentioned = true
messages[idx].MediaUnread = mediaUnread
}
}
if err := rows.Err(); err != nil {
rows.Close()
return err
}
rows.Close()
}
return nil
}
func channelReactionOffset(row domain.ChannelMessagePeerReaction) string {
return strconv.Itoa(row.Date) + ":" + strconv.FormatInt(row.UserID, 10) + ":" + row.Reaction.Emoticon
}
@ -8611,6 +8717,7 @@ func insertChannelUnreadMentionsTx(ctx context.Context, tx pgx.Tx, channelID int
candidates = candidates[:domain.MaxChannelMentionRecipients]
}
topID := channelMentionTopID(msg)
mediaUnread := !msg.Media.IsZero()
if _, err := tx.Exec(ctx, `
WITH input(user_id) AS (
SELECT DISTINCT unnest($4::bigint[])
@ -8626,8 +8733,8 @@ active AS (
LIMIT $6
),
inserted AS (
INSERT INTO channel_unread_mentions (user_id, channel_id, message_id, top_message_id)
SELECT user_id, $1, $2, $3
INSERT INTO channel_unread_mentions (user_id, channel_id, message_id, top_message_id, media_unread)
SELECT user_id, $1, $2, $3, $7
FROM active
ON CONFLICT DO NOTHING
RETURNING user_id
@ -8641,7 +8748,7 @@ ON CONFLICT (user_id, channel_id) DO UPDATE SET
top_message_id = GREATEST(channel_dialogs.top_message_id, EXCLUDED.top_message_id),
top_message_date = GREATEST(channel_dialogs.top_message_date, EXCLUDED.top_message_date),
unread_mentions_count = channel_dialogs.unread_mentions_count + 1,
updated_at = now()`, channelID, msg.ID, topID, candidates, msg.Date, domain.MaxChannelMentionRecipients); err != nil {
updated_at = now()`, channelID, msg.ID, topID, candidates, msg.Date, domain.MaxChannelMentionRecipients, mediaUnread); err != nil {
return fmt.Errorf("insert channel unread mentions: %w", err)
}
return nil

View file

@ -366,6 +366,118 @@ func contactFromFields(id, accessHash int64, phone, firstName, lastName, usernam
}
}
func (s *ContactStore) Block(ctx context.Context, userID, blockedUserID int64, date int) (bool, error) {
if userID == 0 || blockedUserID == 0 || userID == blockedUserID {
return false, nil
}
tag, err := s.db.Exec(ctx, `
INSERT INTO contact_blocks (owner_user_id, blocked_user_id, date)
VALUES ($1, $2, $3)
ON CONFLICT (owner_user_id, blocked_user_id) DO UPDATE SET
date = EXCLUDED.date,
created_at = contact_blocks.created_at`, userID, blockedUserID, date)
if err != nil {
return false, fmt.Errorf("block contact: %w", err)
}
return tag.RowsAffected() > 0, nil
}
func (s *ContactStore) Unblock(ctx context.Context, userID, blockedUserID int64) (bool, error) {
if userID == 0 || blockedUserID == 0 {
return false, nil
}
tag, err := s.db.Exec(ctx, `
DELETE FROM contact_blocks
WHERE owner_user_id = $1
AND blocked_user_id = $2`, userID, blockedUserID)
if err != nil {
return false, fmt.Errorf("unblock contact: %w", err)
}
return tag.RowsAffected() > 0, nil
}
func (s *ContactStore) IsBlocked(ctx context.Context, userID, blockedUserID int64) (bool, error) {
if userID == 0 || blockedUserID == 0 {
return false, nil
}
var blocked bool
if err := s.db.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM contact_blocks
WHERE owner_user_id = $1
AND blocked_user_id = $2
)`, userID, blockedUserID).Scan(&blocked); err != nil {
return false, fmt.Errorf("check contact block: %w", err)
}
return blocked, nil
}
func (s *ContactStore) ListBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error) {
if userID == 0 {
return domain.BlockedContactList{}, nil
}
if offset < 0 {
offset = 0
}
if limit <= 0 || limit > 100 {
limit = 100
}
var count int
if err := s.db.QueryRow(ctx, `
SELECT COUNT(*)::int
FROM contact_blocks
WHERE owner_user_id = $1`, userID).Scan(&count); err != nil {
return domain.BlockedContactList{}, fmt.Errorf("count blocked contacts: %w", err)
}
rows, err := s.db.Query(ctx, `
SELECT
b.blocked_user_id,
b.date,
u.access_hash,
u.phone,
u.first_name,
u.last_name,
u.username,
u.country_code,
u.verified,
u.support,
u.last_seen_at
FROM contact_blocks b
JOIN users u ON u.id = b.blocked_user_id
WHERE b.owner_user_id = $1
ORDER BY b.date DESC, b.blocked_user_id DESC
OFFSET $2
LIMIT $3`, userID, offset, limit)
if err != nil {
return domain.BlockedContactList{}, fmt.Errorf("list blocked contacts: %w", err)
}
defer rows.Close()
out := domain.BlockedContactList{Count: count, Blocked: make([]domain.BlockedContact, 0, limit)}
for rows.Next() {
var item domain.BlockedContact
var lastSeen int64
if err := rows.Scan(
&item.User.ID,
&item.Date,
&item.User.AccessHash,
&item.User.Phone,
&item.User.FirstName,
&item.User.LastName,
&item.User.Username,
&item.User.CountryCode,
&item.User.Verified,
&item.User.Support,
&lastSeen,
); err != nil {
return domain.BlockedContactList{}, err
}
item.User.LastSeenAt = int(lastSeen)
out.Blocked = append(out.Blocked, item)
}
return out, rows.Err()
}
func contactListHash(contacts []domain.Contact) int64 {
if len(contacts) == 0 {
return 0

View file

@ -175,7 +175,8 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
var recipientBoxID, recipientPts int
selfMessage := req.RecipientUserID == req.SenderUserID
if !selfMessage {
deliverRecipient := !selfMessage && !req.RecipientBlocked
if deliverRecipient {
recipientBoxID, err = s.boxIDs.NextBoxID(ctx, req.RecipientUserID)
if err != nil {
s.recordPtsGaps(ctx, reserved, req.Date)
@ -249,6 +250,8 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
EntitiesJson: entities,
Pts: int32(senderPts),
MediaJson: mediaJSON,
MediaUnread: false,
ReactionUnread: false,
}
applyCreateMessageBoxMetadata(&senderArg, senderMeta)
senderRow, err := qtx.CreateMessageBox(ctx, senderArg)
@ -278,8 +281,11 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
return domain.SendPrivateTextResult{}, fmt.Errorf("enqueue sender dispatch: %w", err)
}
recipient := sender
if !selfMessage {
recipient := domain.Message{}
if selfMessage {
recipient = sender
}
if deliverRecipient {
recipientArg := sqlcgen.CreateMessageBoxParams{
OwnerUserID: req.RecipientUserID,
BoxID: int32(recipientBoxID),
@ -294,6 +300,8 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
EntitiesJson: entities,
Pts: int32(recipientPts),
MediaJson: mediaJSON,
MediaUnread: !req.Media.IsZero(),
ReactionUnread: false,
}
applyCreateMessageBoxMetadata(&recipientArg, recipientMeta)
recipientRow, err := qtx.CreateMessageBox(ctx, recipientArg)
@ -352,13 +360,23 @@ func (s *MessageStore) duplicateSendResult(ctx context.Context, senderUserID, re
return domain.SendPrivateTextResult{}, fmt.Errorf("get duplicate sender box: %w", err)
}
sender := messageFromGetBoxRow(senderRow)
recipient := sender
recipient := domain.Message{}
if recipientUserID == senderUserID {
recipient = sender
}
if recipientUserID != senderUserID {
recipientRow, err := s.q.GetMessageBoxByPrivateMessage(ctx, sqlcgen.GetMessageBoxByPrivateMessageParams{
OwnerUserID: recipientUserID,
PrivateMessageID: pm.ID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.SendPrivateTextResult{
SenderMessage: sender,
SenderEvent: eventFromMessage(sender),
RecipientEvent: domain.UpdateEvent{},
}, nil
}
return domain.SendPrivateTextResult{}, fmt.Errorf("get duplicate recipient box: %w", err)
}
recipient = messageFromGetBoxRow(recipientRow)
@ -514,19 +532,20 @@ func (s *MessageStore) ForwardPrivateMessages(ctx context.Context, req domain.Fo
}
}
sent, err := s.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: req.OwnerUserID,
RecipientUserID: req.ToUserID,
RandomID: req.RandomIDs[i],
Message: source.Body,
Entities: append([]domain.MessageEntity(nil), source.Entities...),
Media: source.Media,
Silent: req.Silent,
NoForwards: req.NoForwards,
ReplyTo: req.ReplyTo,
Forward: forward,
Date: req.Date,
OriginAuthKeyID: req.OriginAuthKeyID,
OriginSessionID: req.OriginSessionID,
SenderUserID: req.OwnerUserID,
RecipientUserID: req.ToUserID,
RandomID: req.RandomIDs[i],
Message: source.Body,
Entities: append([]domain.MessageEntity(nil), source.Entities...),
Media: source.Media,
Silent: req.Silent,
NoForwards: req.NoForwards,
ReplyTo: req.ReplyTo,
Forward: forward,
Date: req.Date,
OriginAuthKeyID: req.OriginAuthKeyID,
OriginSessionID: req.OriginSessionID,
RecipientBlocked: req.RecipientBlocked,
})
if err != nil {
return res, err
@ -640,22 +659,24 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
return domain.MessageList{}, fmt.Errorf("decode message media: %w", err)
}
out.Messages = append(out.Messages, domain.Message{
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
})
if out.Count == 0 {
out.Count = int(row.TotalCount)
@ -832,6 +853,9 @@ func (s *MessageStore) ReadMessageContents(ctx context.Context, req domain.ReadM
if req.OwnerUserID == 0 {
return res, fmt.Errorf("read message contents: missing owner user id")
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
if len(req.IDs) > domain.MaxGetMessageIDs {
return res, domain.ErrMessageIDInvalid
}
@ -850,27 +874,128 @@ func (s *MessageStore) ReadMessageContents(ctx context.Context, req domain.ReadM
if len(ids) == 0 {
return res, nil
}
rows, err := s.db.Query(ctx, `
SELECT box_id
FROM message_boxes
WHERE owner_user_id = $1
AND box_id = ANY($2::int[])
AND NOT deleted
beginner, ok := s.db.(txBeginner)
if !ok {
return res, fmt.Errorf("read message contents: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return res, fmt.Errorf("begin read message contents tx: %w", err)
}
qtx := sqlcgen.New(tx)
committed := false
var reserved []reservedPts
defer func() {
if committed {
return
}
_ = tx.Rollback(ctx)
s.recordPtsGaps(ctx, reserved, req.Date)
}()
if err := lockUsersForUpdate(ctx, tx, req.OwnerUserID); err != nil {
return res, fmt.Errorf("lock read message contents user: %w", err)
}
rows, err := tx.Query(ctx, `
WITH target AS (
SELECT owner_user_id, box_id, peer_type, peer_id, reaction_unread
FROM message_boxes
WHERE owner_user_id = $1
AND box_id = ANY($2::int[])
AND NOT deleted
AND (media_unread OR reaction_unread)
FOR UPDATE
),
updated AS (
UPDATE message_boxes
SET media_unread = false,
reaction_unread = false
FROM target t
WHERE message_boxes.owner_user_id = t.owner_user_id
AND message_boxes.box_id = t.box_id
RETURNING message_boxes.box_id, t.peer_type, t.peer_id, t.reaction_unread
)
SELECT box_id, peer_type, peer_id, reaction_unread
FROM updated
ORDER BY box_id`, req.OwnerUserID, ids)
if err != nil {
return res, fmt.Errorf("read message contents: %w", err)
}
defer rows.Close()
affectedPeers := make(map[domain.Peer]struct{})
for rows.Next() {
var id int32
if err := rows.Scan(&id); err != nil {
var peerType string
var peerID int64
var reactionUnread bool
if err := rows.Scan(&id, &peerType, &peerID, &reactionUnread); err != nil {
return res, fmt.Errorf("scan read message contents: %w", err)
}
res.MessageIDs = append(res.MessageIDs, int(id))
if reactionUnread && peerID != 0 {
affectedPeers[domain.Peer{Type: domain.PeerType(peerType), ID: peerID}] = struct{}{}
}
}
if err := rows.Err(); err != nil {
return res, fmt.Errorf("read message contents rows: %w", err)
}
if len(res.MessageIDs) == 0 {
if err := tx.Commit(ctx); err != nil {
return res, fmt.Errorf("commit read message contents noop: %w", err)
}
committed = true
return res, nil
}
for peer := range affectedPeers {
if peer.Type != domain.PeerTypeUser || peer.ID == 0 {
continue
}
if _, err := tx.Exec(ctx, `
UPDATE dialogs d
SET unread_reactions_count = (
SELECT COUNT(*)::int
FROM message_boxes m
WHERE m.owner_user_id = d.user_id
AND m.peer_type = d.peer_type
AND m.peer_id = d.peer_id
AND NOT m.deleted
AND m.reaction_unread
),
updated_at = now()
WHERE d.user_id = $1
AND d.peer_type = $2
AND d.peer_id = $3`, req.OwnerUserID, string(peer.Type), peer.ID); err != nil {
return res, fmt.Errorf("refresh dialog unread reactions after content read: %w", err)
}
}
pts, err := s.nextPtsN(ctx, req.OwnerUserID, len(res.MessageIDs))
if err != nil {
return res, fmt.Errorf("allocate read message contents pts: %w", err)
}
reserved = append(reserved, reservedPts{userID: req.OwnerUserID, pts: pts, count: len(res.MessageIDs)})
res.Event = domain.UpdateEvent{
UserID: req.OwnerUserID,
Type: domain.UpdateEventReadMessageContents,
Pts: pts,
PtsCount: len(res.MessageIDs),
Date: req.Date,
MessageIDs: append([]int(nil), res.MessageIDs...),
}
if err := appendUserUpdateEvent(ctx, qtx, req.OwnerUserID, res.Event); err != nil {
return res, fmt.Errorf("append read message contents event: %w", err)
}
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
TargetUserID: req.OwnerUserID,
Pts: int32(pts),
EventType: string(domain.UpdateEventReadMessageContents),
ExcludeAuthKeyID: authKeyIDToInt64(req.OriginAuthKeyID),
ExcludeSessionID: req.OriginSessionID,
}); err != nil {
return res, fmt.Errorf("enqueue read message contents dispatch: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return res, fmt.Errorf("commit read message contents tx: %w", err)
}
committed = true
return res, nil
}
@ -996,6 +1121,39 @@ DO UPDATE SET
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("insert message reaction: %w", err)
}
}
if target.messageSenderID != 0 && target.messageSenderID != req.UserID {
if _, err := tx.Exec(ctx, `
UPDATE message_boxes b
SET reaction_unread = EXISTS (
SELECT 1
FROM private_message_reactions r
WHERE r.message_sender_id = b.message_sender_id
AND r.private_message_id = b.private_message_id
AND r.user_id <> b.owner_user_id
)
WHERE b.owner_user_id = $1
AND b.message_sender_id = $2
AND b.private_message_id = $3`, target.messageSenderID, target.messageSenderID, target.privateMessageID); err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("update private reaction unread: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE dialogs d
SET unread_reactions_count = (
SELECT COUNT(*)::int
FROM message_boxes m
WHERE m.owner_user_id = d.user_id
AND m.peer_type = d.peer_type
AND m.peer_id = d.peer_id
AND NOT m.deleted
AND m.reaction_unread
),
updated_at = now()
WHERE d.user_id = $1
AND d.peer_type = $2
AND d.peer_id = $3`, target.messageSenderID, string(domain.PeerTypeUser), req.UserID); err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("refresh private reaction unread dialog: %w", err)
}
}
boxes, err := qtx.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
MessageSenderID: target.messageSenderID,
@ -1911,22 +2069,24 @@ func messageFromBoxRow(row sqlcgen.CreateMessageBoxRow) domain.Message {
row.FwdDate,
)
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}
}
@ -1949,22 +2109,24 @@ func messageFromGetBoxRow(row sqlcgen.GetMessageBoxByPrivateMessageRow) domain.M
row.FwdDate,
)
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}
}
@ -1996,22 +2158,24 @@ func messageFromVisibleBoxRow(row sqlcgen.ListVisibleMessageBoxesByPrivateMessag
return domain.Message{}, fmt.Errorf("decode visible message media: %w", err)
}
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}, nil
}
@ -2043,22 +2207,24 @@ func messageFromUpdateEditRow(row sqlcgen.UpdateMessageBoxEditRow) (domain.Messa
return domain.Message{}, fmt.Errorf("decode edited message media: %w", err)
}
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}, nil
}
@ -2090,22 +2256,24 @@ func messageFromForwardRow(row sqlcgen.GetMessageBoxesForForwardRow) (domain.Mes
return domain.Message{}, fmt.Errorf("decode forward message media: %w", err)
}
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}, nil
}
@ -2137,22 +2305,24 @@ func messageFromIDRow(row sqlcgen.GetMessageBoxesByIDsRow) (domain.Message, erro
return domain.Message{}, fmt.Errorf("decode message media: %w", err)
}
return domain.Message{
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
Media: media,
ID: int(row.BoxID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Pts: int(row.Pts),
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
}, nil
}

View file

@ -167,7 +167,9 @@ INSERT INTO message_boxes (
fwd_from_name,
fwd_date,
pts,
media
media,
media_unread,
reaction_unread
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, sqlc.arg(entities_json)::jsonb,
sqlc.arg(silent)::boolean,
@ -184,7 +186,9 @@ INSERT INTO message_boxes (
sqlc.arg(fwd_from_name)::text,
sqlc.arg(fwd_date)::int,
sqlc.arg(pts)::int,
sqlc.arg(media_json)::jsonb
sqlc.arg(media_json)::jsonb,
sqlc.arg(media_unread)::boolean,
sqlc.arg(reaction_unread)::boolean
)
RETURNING
box_id,
@ -212,7 +216,9 @@ RETURNING
fwd_from_name,
fwd_date,
pts,
media::text AS media_json;
media::text AS media_json,
media_unread,
reaction_unread;
-- name: GetMessageBoxByPrivateMessage :one
SELECT
@ -241,7 +247,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE owner_user_id = $1
AND private_message_id = $2
@ -295,7 +303,9 @@ SELECT
m.fwd_from_name,
m.fwd_date,
m.pts,
m.media::text AS media_json
m.media::text AS media_json,
m.media_unread,
m.reaction_unread
FROM requested r
JOIN message_boxes m
ON m.owner_user_id = sqlc.arg(owner_user_id)::bigint
@ -351,6 +361,8 @@ base AS NOT MATERIALIZED (
m.fwd_date,
m.pts,
m.media::text AS media_json,
m.media_unread,
m.reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -483,6 +495,8 @@ SELECT
fwd_date,
pts,
media_json,
media_unread,
reaction_unread,
peer_user_id,
peer_access_hash,
peer_phone,
@ -537,6 +551,8 @@ SELECT
m.fwd_date,
m.pts,
m.media::text AS media_json,
m.media_unread,
m.reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -594,7 +610,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE owner_user_id = sqlc.arg(owner_user_id)::bigint
AND box_id = sqlc.arg(box_id)::int
@ -632,7 +650,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE message_sender_id = sqlc.arg(message_sender_id)::bigint
AND private_message_id = sqlc.arg(private_message_id)::bigint
@ -684,7 +704,9 @@ RETURNING
fwd_from_name,
fwd_date,
pts,
media::text AS media_json;
media::text AS media_json,
media_unread,
reaction_unread;
-- name: GetDialogReadStateForUpdate :one
SELECT

View file

@ -87,6 +87,8 @@ SELECT
COALESCE(m.fwd_from_name, '')::text AS fwd_from_name,
COALESCE(m.fwd_date, 0)::int AS fwd_date,
COALESCE(m.media::text, '{}')::text AS media_json,
COALESCE(m.media_unread, false)::boolean AS media_unread,
COALESCE(m.reaction_unread, false)::boolean AS reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -341,6 +343,8 @@ SELECT
COALESCE(m.fwd_from_name, '')::text AS fwd_from_name,
COALESCE(m.fwd_date, 0)::int AS fwd_date,
COALESCE(m.media::text, '{}')::text AS media_json,
COALESCE(m.media_unread, false)::boolean AS media_unread,
COALESCE(m.reaction_unread, false)::boolean AS reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,

View file

@ -173,7 +173,9 @@ INSERT INTO message_boxes (
fwd_from_name,
fwd_date,
pts,
media
media,
media_unread,
reaction_unread
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb,
$12::boolean,
@ -190,7 +192,9 @@ INSERT INTO message_boxes (
$23::text,
$24::int,
$25::int,
$26::jsonb
$26::jsonb,
$27::boolean,
$28::boolean
)
RETURNING
box_id,
@ -218,7 +222,9 @@ RETURNING
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
`
type CreateMessageBoxParams struct {
@ -248,6 +254,8 @@ type CreateMessageBoxParams struct {
FwdDate int32
Pts int32
MediaJson []byte
MediaUnread bool
ReactionUnread bool
}
type CreateMessageBoxRow struct {
@ -277,6 +285,8 @@ type CreateMessageBoxRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) CreateMessageBox(ctx context.Context, arg CreateMessageBoxParams) (CreateMessageBoxRow, error) {
@ -307,6 +317,8 @@ func (q *Queries) CreateMessageBox(ctx context.Context, arg CreateMessageBoxPara
arg.FwdDate,
arg.Pts,
arg.MediaJson,
arg.MediaUnread,
arg.ReactionUnread,
)
var i CreateMessageBoxRow
err := row.Scan(
@ -336,6 +348,8 @@ func (q *Queries) CreateMessageBox(ctx context.Context, arg CreateMessageBoxPara
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
)
return i, err
}
@ -845,7 +859,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE owner_user_id = $1
AND private_message_id = $2
@ -884,6 +900,8 @@ type GetMessageBoxByPrivateMessageRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) GetMessageBoxByPrivateMessage(ctx context.Context, arg GetMessageBoxByPrivateMessageParams) (GetMessageBoxByPrivateMessageRow, error) {
@ -916,6 +934,8 @@ func (q *Queries) GetMessageBoxByPrivateMessage(ctx context.Context, arg GetMess
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
)
return i, err
}
@ -948,7 +968,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE owner_user_id = $1::bigint
AND box_id = $2::int
@ -994,6 +1016,8 @@ type GetMessageBoxForEditRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) GetMessageBoxForEdit(ctx context.Context, arg GetMessageBoxForEditParams) (GetMessageBoxForEditRow, error) {
@ -1032,6 +1056,8 @@ func (q *Queries) GetMessageBoxForEdit(ctx context.Context, arg GetMessageBoxFor
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
)
return i, err
}
@ -1104,6 +1130,8 @@ SELECT
m.fwd_date,
m.pts,
m.media::text AS media_json,
m.media_unread,
m.reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -1167,6 +1195,8 @@ type GetMessageBoxesByIDsRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
PeerUserID int64
PeerAccessHash int64
PeerPhone string
@ -1226,6 +1256,8 @@ func (q *Queries) GetMessageBoxesByIDs(ctx context.Context, arg GetMessageBoxesB
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
&i.PeerUserID,
&i.PeerAccessHash,
&i.PeerPhone,
@ -1292,7 +1324,9 @@ SELECT
m.fwd_from_name,
m.fwd_date,
m.pts,
m.media::text AS media_json
m.media::text AS media_json,
m.media_unread,
m.reaction_unread
FROM requested r
JOIN message_boxes m
ON m.owner_user_id = $1::bigint
@ -1339,6 +1373,8 @@ type GetMessageBoxesForForwardRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) GetMessageBoxesForForward(ctx context.Context, arg GetMessageBoxesForForwardParams) ([]GetMessageBoxesForForwardRow, error) {
@ -1384,6 +1420,8 @@ func (q *Queries) GetMessageBoxesForForward(ctx context.Context, arg GetMessageB
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
); err != nil {
return nil, err
}
@ -1633,6 +1671,8 @@ base AS NOT MATERIALIZED (
m.fwd_date,
m.pts,
m.media::text AS media_json,
m.media_unread,
m.reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -1675,7 +1715,7 @@ total AS (
WHERE $12::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.edit_date, 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.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.pts, b.media_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_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_last_seen_at
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.edit_date, 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.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.pts, b.media_json, b.media_unread, b.reaction_unread, 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_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_last_seen_at
FROM base b
CROSS JOIN load_params p
WHERE p.load_type = 'backward'
@ -1688,9 +1728,9 @@ backward AS (
LIMIT (SELECT limit_count FROM load_params)
),
around_forward AS (
SELECT f.box_id, f.private_message_id, f.owner_user_id, f.peer_type, f.peer_id, f.from_user_id, f.message_date, f.edit_date, f.outgoing, f.body, f.entities_json, f.silent, f.noforwards, f.reply_to_msg_id, f.reply_to_peer_type, f.reply_to_peer_id, f.reply_to_top_id, f.quote_text, f.quote_entities_json, f.quote_offset, f.fwd_from_peer_type, f.fwd_from_peer_id, f.fwd_from_name, f.fwd_date, f.pts, f.media_json, f.peer_user_id, f.peer_access_hash, f.peer_phone, f.peer_first_name, f.peer_last_name, f.peer_username, f.peer_country_code, f.peer_verified, f.peer_support, f.peer_last_seen_at, f.from_user_user_id, f.from_user_access_hash, f.from_user_phone, f.from_user_first_name, f.from_user_last_name, f.from_user_username, f.from_user_country_code, f.from_user_verified, f.from_user_support, f.from_user_last_seen_at
SELECT f.box_id, f.private_message_id, f.owner_user_id, f.peer_type, f.peer_id, f.from_user_id, f.message_date, f.edit_date, f.outgoing, f.body, f.entities_json, f.silent, f.noforwards, f.reply_to_msg_id, f.reply_to_peer_type, f.reply_to_peer_id, f.reply_to_top_id, f.quote_text, f.quote_entities_json, f.quote_offset, f.fwd_from_peer_type, f.fwd_from_peer_id, f.fwd_from_name, f.fwd_date, f.pts, f.media_json, f.media_unread, f.reaction_unread, f.peer_user_id, f.peer_access_hash, f.peer_phone, f.peer_first_name, f.peer_last_name, f.peer_username, f.peer_country_code, f.peer_verified, f.peer_support, f.peer_last_seen_at, f.from_user_user_id, f.from_user_access_hash, f.from_user_phone, f.from_user_first_name, f.from_user_last_name, f.from_user_username, f.from_user_country_code, f.from_user_verified, f.from_user_support, f.from_user_last_seen_at
FROM (
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.edit_date, 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.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.pts, b.media_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_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_last_seen_at
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.edit_date, 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.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.pts, b.media_json, b.media_unread, b.reaction_unread, 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_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_last_seen_at
FROM base b
CROSS JOIN load_params p
WHERE p.load_type = 'around'
@ -1703,7 +1743,7 @@ around_forward AS (
) f
),
around_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.edit_date, 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.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.pts, b.media_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_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_last_seen_at
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.edit_date, 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.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.pts, b.media_json, b.media_unread, b.reaction_unread, 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_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_last_seen_at
FROM base b
CROSS JOIN load_params p
WHERE p.load_type = 'around'
@ -1715,9 +1755,9 @@ around_backward AS (
LIMIT GREATEST((SELECT limit_count + add_offset FROM load_params), 0)
),
forward AS (
SELECT f.box_id, f.private_message_id, f.owner_user_id, f.peer_type, f.peer_id, f.from_user_id, f.message_date, f.edit_date, f.outgoing, f.body, f.entities_json, f.silent, f.noforwards, f.reply_to_msg_id, f.reply_to_peer_type, f.reply_to_peer_id, f.reply_to_top_id, f.quote_text, f.quote_entities_json, f.quote_offset, f.fwd_from_peer_type, f.fwd_from_peer_id, f.fwd_from_name, f.fwd_date, f.pts, f.media_json, f.peer_user_id, f.peer_access_hash, f.peer_phone, f.peer_first_name, f.peer_last_name, f.peer_username, f.peer_country_code, f.peer_verified, f.peer_support, f.peer_last_seen_at, f.from_user_user_id, f.from_user_access_hash, f.from_user_phone, f.from_user_first_name, f.from_user_last_name, f.from_user_username, f.from_user_country_code, f.from_user_verified, f.from_user_support, f.from_user_last_seen_at
SELECT f.box_id, f.private_message_id, f.owner_user_id, f.peer_type, f.peer_id, f.from_user_id, f.message_date, f.edit_date, f.outgoing, f.body, f.entities_json, f.silent, f.noforwards, f.reply_to_msg_id, f.reply_to_peer_type, f.reply_to_peer_id, f.reply_to_top_id, f.quote_text, f.quote_entities_json, f.quote_offset, f.fwd_from_peer_type, f.fwd_from_peer_id, f.fwd_from_name, f.fwd_date, f.pts, f.media_json, f.media_unread, f.reaction_unread, f.peer_user_id, f.peer_access_hash, f.peer_phone, f.peer_first_name, f.peer_last_name, f.peer_username, f.peer_country_code, f.peer_verified, f.peer_support, f.peer_last_seen_at, f.from_user_user_id, f.from_user_access_hash, f.from_user_phone, f.from_user_first_name, f.from_user_last_name, f.from_user_username, f.from_user_country_code, f.from_user_verified, f.from_user_support, f.from_user_last_seen_at
FROM (
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.edit_date, 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.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.pts, b.media_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_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_last_seen_at
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.edit_date, 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.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.pts, b.media_json, b.media_unread, b.reaction_unread, 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_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_last_seen_at
FROM base b
CROSS JOIN load_params p
WHERE p.load_type = 'forward'
@ -1730,13 +1770,13 @@ forward AS (
) f
),
paged AS (
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM backward
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, media_unread, reaction_unread, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM backward
UNION ALL
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM around_forward
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, media_unread, reaction_unread, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM around_forward
UNION ALL
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM around_backward
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, media_unread, reaction_unread, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM around_backward
UNION ALL
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM forward
SELECT box_id, private_message_id, owner_user_id, peer_type, peer_id, from_user_id, message_date, edit_date, outgoing, body, entities_json, silent, noforwards, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, quote_text, quote_entities_json, quote_offset, fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date, pts, media_json, media_unread, reaction_unread, peer_user_id, peer_access_hash, peer_phone, peer_first_name, peer_last_name, peer_username, peer_country_code, peer_verified, peer_support, peer_last_seen_at, from_user_user_id, from_user_access_hash, from_user_phone, from_user_first_name, from_user_last_name, from_user_username, from_user_country_code, from_user_verified, from_user_support, from_user_last_seen_at FROM forward
)
SELECT
box_id,
@ -1765,6 +1805,8 @@ SELECT
fwd_date,
pts,
media_json,
media_unread,
reaction_unread,
peer_user_id,
peer_access_hash,
peer_phone,
@ -1833,6 +1875,8 @@ type ListMessagesByUserRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
PeerUserID int64
PeerAccessHash int64
PeerPhone string
@ -1905,6 +1949,8 @@ func (q *Queries) ListMessagesByUser(ctx context.Context, arg ListMessagesByUser
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
&i.PeerUserID,
&i.PeerAccessHash,
&i.PeerPhone,
@ -1965,7 +2011,9 @@ SELECT
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
FROM message_boxes
WHERE message_sender_id = $1::bigint
AND private_message_id = $2::bigint
@ -2007,6 +2055,8 @@ type ListVisibleMessageBoxesByPrivateMessageRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) ListVisibleMessageBoxesByPrivateMessage(ctx context.Context, arg ListVisibleMessageBoxesByPrivateMessageParams) ([]ListVisibleMessageBoxesByPrivateMessageRow, error) {
@ -2046,6 +2096,8 @@ func (q *Queries) ListVisibleMessageBoxesByPrivateMessage(ctx context.Context, a
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
); err != nil {
return nil, err
}
@ -2218,7 +2270,9 @@ RETURNING
fwd_from_name,
fwd_date,
pts,
media::text AS media_json
media::text AS media_json,
media_unread,
reaction_unread
`
type UpdateMessageBoxEditParams struct {
@ -2258,6 +2312,8 @@ type UpdateMessageBoxEditRow struct {
FwdDate int32
Pts int32
MediaJson string
MediaUnread bool
ReactionUnread bool
}
func (q *Queries) UpdateMessageBoxEdit(ctx context.Context, arg UpdateMessageBoxEditParams) (UpdateMessageBoxEditRow, error) {
@ -2298,6 +2354,8 @@ func (q *Queries) UpdateMessageBoxEdit(ctx context.Context, arg UpdateMessageBox
&i.FwdDate,
&i.Pts,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
)
return i, err
}

View file

@ -320,6 +320,7 @@ type ChannelUnreadMention struct {
MessageID int32
TopMessageID int32
CreatedAt pgtype.Timestamptz
MediaUnread bool
}
type ChannelUpdateEvent struct {
@ -357,6 +358,13 @@ type Contact struct {
StoriesHidden bool
}
type ContactBlock struct {
OwnerUserID int64
BlockedUserID int64
Date int32
CreatedAt pgtype.Timestamptz
}
type Country struct {
Iso2 string
DefaultName string
@ -514,6 +522,8 @@ type MessageBox struct {
FwdFromName string
FwdDate int32
Media []byte
MediaUnread bool
ReactionUnread bool
}
type Photo struct {

View file

@ -146,6 +146,8 @@ SELECT
COALESCE(m.fwd_from_name, '')::text AS fwd_from_name,
COALESCE(m.fwd_date, 0)::int AS fwd_date,
COALESCE(m.media::text, '{}')::text AS media_json,
COALESCE(m.media_unread, false)::boolean AS media_unread,
COALESCE(m.reaction_unread, false)::boolean AS reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -291,6 +293,8 @@ type BatchListDispatchEventsRow struct {
FwdFromName string
FwdDate int32
MediaJson string
MediaUnread bool
ReactionUnread bool
PeerUserID int64
PeerAccessHash int64
PeerPhone string
@ -432,6 +436,8 @@ func (q *Queries) BatchListDispatchEvents(ctx context.Context, arg BatchListDisp
&i.FwdFromName,
&i.FwdDate,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
&i.PeerUserID,
&i.PeerAccessHash,
&i.PeerPhone,
@ -737,6 +743,8 @@ SELECT
COALESCE(m.fwd_from_name, '')::text AS fwd_from_name,
COALESCE(m.fwd_date, 0)::int AS fwd_date,
COALESCE(m.media::text, '{}')::text AS media_json,
COALESCE(m.media_unread, false)::boolean AS media_unread,
COALESCE(m.reaction_unread, false)::boolean AS reaction_unread,
COALESCE(peer_u.id, 0)::bigint AS peer_user_id,
COALESCE(peer_u.access_hash, 0)::bigint AS peer_access_hash,
COALESCE(peer_u.phone, '')::text AS peer_phone,
@ -885,6 +893,8 @@ type ListUserUpdateEventsAfterRow struct {
FwdFromName string
FwdDate int32
MediaJson string
MediaUnread bool
ReactionUnread bool
PeerUserID int64
PeerAccessHash int64
PeerPhone string
@ -1024,6 +1034,8 @@ func (q *Queries) ListUserUpdateEventsAfter(ctx context.Context, arg ListUserUpd
&i.FwdFromName,
&i.FwdDate,
&i.MediaJson,
&i.MediaUnread,
&i.ReactionUnread,
&i.PeerUserID,
&i.PeerAccessHash,
&i.PeerPhone,

View file

@ -260,21 +260,23 @@ func (s *UpdateEventStore) ListAfter(ctx context.Context, userID int64, pts, lim
FolderPeers: folderPeers,
TagsEnabled: row.TagsEnabled,
Message: domain.Message{
ID: int(row.MessageID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Media: media,
ID: int(row.MessageID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Media: media,
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
},
Users: usersFromUpdateEventRow(row),
Channels: channelsFromUpdateEventRow(row),
@ -519,21 +521,23 @@ func (s *UpdateEventStore) BatchByCursor(ctx context.Context, cursors []store.Ev
FolderPeers: folderPeers,
TagsEnabled: row.TagsEnabled,
Message: domain.Message{
ID: int(row.MessageID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Media: media,
ID: int(row.MessageID),
UID: row.PrivateMessageID,
OwnerUserID: row.OwnerUserID,
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.FromUserID},
Date: int(row.MessageDate),
EditDate: int(row.EditDate),
Out: row.Outgoing,
Silent: silent,
NoForwards: noforwards,
Body: row.Body,
Entities: entities,
ReplyTo: reply,
Forward: forward,
Media: media,
MediaUnread: row.MediaUnread,
ReactionUnread: row.ReactionUnread,
},
Users: usersFromBatchDispatchRow(row),
Channels: channelsFromBatchDispatchRow(row),