482 lines
18 KiB
Go
482 lines
18 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/postgres/sqlcgen"
|
||
)
|
||
|
||
// ListSavedDialogs 返回收藏夹子会话分页:首页(offset 无效)且不排除置顶时
|
||
// 置顶块按 pinned_order 在前,普通块按 top box id 严格降序;翻页恒排除置顶
|
||
// (置顶只随首页返回,避免跨页重复)。Count 按过滤口径(含/不含置顶)。
|
||
func (s *MessageStore) ListSavedDialogs(ctx context.Context, userID int64, filter domain.SavedDialogsFilter) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list saved dialogs: missing user id")
|
||
}
|
||
limit := filter.Limit
|
||
if limit <= 0 || limit > domain.MaxSavedDialogsLimit {
|
||
limit = domain.MaxSavedDialogsLimit
|
||
}
|
||
// TDesktop 首页 offset_id=0,DrKLO Android 首页 offset_id=int32 max;
|
||
// 两者都视为"从最新开始"。
|
||
offsetID := filter.OffsetID
|
||
// DrKLO Android 首页恰好发 offset_id = MaxMessageBoxID(int32 max),必须用 >=
|
||
// 才能命中首页分支;否则该值被误判为续页,置顶子会话块被整体跳过、首页丢置顶。
|
||
firstPage := offsetID <= 0 || offsetID >= domain.MaxMessageBoxID
|
||
if firstPage {
|
||
offsetID = 0
|
||
}
|
||
if firstPage && !filter.ExcludePinned {
|
||
rows, err := s.q.ListPinnedSavedDialogTops(ctx, userID)
|
||
if err != nil {
|
||
return out, fmt.Errorf("list pinned saved dialog tops: %w", err)
|
||
}
|
||
for _, row := range rows {
|
||
if len(out.Dialogs) >= limit {
|
||
break
|
||
}
|
||
msg, err := messageFromSavedDialogRow(savedDialogRowFields(row))
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
out.Dialogs = append(out.Dialogs, domain.SavedDialog{
|
||
Peer: domain.Peer{Type: domain.PeerType(row.DialogPeerType), ID: row.DialogPeerID},
|
||
TopMessage: msg.ID,
|
||
Pinned: true,
|
||
})
|
||
out.Messages = append(out.Messages, msg)
|
||
}
|
||
}
|
||
remaining := limit - len(out.Dialogs)
|
||
hasMore := false
|
||
if remaining > 0 {
|
||
rows, err := s.q.ListSavedDialogTops(ctx, sqlcgen.ListSavedDialogTopsParams{
|
||
OwnerUserID: userID,
|
||
OffsetID: pgInt32NonNegative(offsetID),
|
||
// 普通块恒排除置顶:置顶要么已随首页置顶块返回,要么被
|
||
// exclude_pinned 显式排除,任何一页都不允许再出现。
|
||
ExcludePinned: true,
|
||
LimitCount: int32(remaining + 1),
|
||
})
|
||
if err != nil {
|
||
return out, fmt.Errorf("list saved dialog tops: %w", err)
|
||
}
|
||
if len(rows) > remaining {
|
||
hasMore = true
|
||
rows = rows[:remaining]
|
||
}
|
||
for _, row := range rows {
|
||
msg, err := messageFromSavedDialogRow(savedDialogRowFields(row))
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
out.Dialogs = append(out.Dialogs, domain.SavedDialog{
|
||
Peer: domain.Peer{Type: domain.PeerType(row.DialogPeerType), ID: row.DialogPeerID},
|
||
TopMessage: msg.ID,
|
||
Pinned: row.DialogPinned,
|
||
})
|
||
out.Messages = append(out.Messages, msg)
|
||
}
|
||
}
|
||
total, err := s.q.CountSavedDialogs(ctx, sqlcgen.CountSavedDialogsParams{
|
||
OwnerUserID: userID,
|
||
ExcludePinned: filter.ExcludePinned,
|
||
})
|
||
if err != nil {
|
||
return out, fmt.Errorf("count saved dialogs: %w", err)
|
||
}
|
||
out.Count = int(total)
|
||
out.Full = !hasMore
|
||
if err := s.enrichPrivateMessageReactions(ctx, s.db, userID, out.Messages); err != nil {
|
||
return out, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// ListPinnedSavedDialogs 返回全部置顶子会话(messages.getPinnedSavedDialogs)。
|
||
func (s *MessageStore) ListPinnedSavedDialogs(ctx context.Context, userID int64) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{Full: true}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list pinned saved dialogs: missing user id")
|
||
}
|
||
rows, err := s.q.ListPinnedSavedDialogTops(ctx, userID)
|
||
if err != nil {
|
||
return out, fmt.Errorf("list pinned saved dialog tops: %w", err)
|
||
}
|
||
for _, row := range rows {
|
||
msg, err := messageFromSavedDialogRow(savedDialogRowFields(row))
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
out.Dialogs = append(out.Dialogs, domain.SavedDialog{
|
||
Peer: domain.Peer{Type: domain.PeerType(row.DialogPeerType), ID: row.DialogPeerID},
|
||
TopMessage: msg.ID,
|
||
Pinned: true,
|
||
})
|
||
out.Messages = append(out.Messages, msg)
|
||
}
|
||
out.Count = len(out.Dialogs)
|
||
if err := s.enrichPrivateMessageReactions(ctx, s.db, userID, out.Messages); err != nil {
|
||
return out, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// ListSavedDialogsByPeers 返回指定子会话(messages.getSavedDialogsByID);
|
||
// 不存在的 peer 静默缺席。
|
||
func (s *MessageStore) ListSavedDialogsByPeers(ctx context.Context, userID int64, peers []domain.Peer) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{Full: true}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list saved dialogs by peers: missing user id")
|
||
}
|
||
peerTypes := make([]string, 0, len(peers))
|
||
peerIDs := make([]int64, 0, len(peers))
|
||
for _, peer := range peers {
|
||
if peer.Type == "" || peer.ID == 0 {
|
||
continue
|
||
}
|
||
peerTypes = append(peerTypes, string(peer.Type))
|
||
peerIDs = append(peerIDs, peer.ID)
|
||
}
|
||
if len(peerTypes) == 0 {
|
||
return out, nil
|
||
}
|
||
rows, err := s.q.ListSavedDialogTopsByPeers(ctx, sqlcgen.ListSavedDialogTopsByPeersParams{
|
||
OwnerUserID: userID,
|
||
PeerTypes: peerTypes,
|
||
PeerIds: peerIDs,
|
||
})
|
||
if err != nil {
|
||
return out, fmt.Errorf("list saved dialog tops by peers: %w", err)
|
||
}
|
||
for _, row := range rows {
|
||
msg, err := messageFromSavedDialogRow(savedDialogRowFields(row))
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
out.Dialogs = append(out.Dialogs, domain.SavedDialog{
|
||
Peer: domain.Peer{Type: domain.PeerType(row.DialogPeerType), ID: row.DialogPeerID},
|
||
TopMessage: msg.ID,
|
||
Pinned: row.DialogPinned,
|
||
})
|
||
out.Messages = append(out.Messages, msg)
|
||
}
|
||
out.Count = len(out.Dialogs)
|
||
if err := s.enrichPrivateMessageReactions(ctx, s.db, userID, out.Messages); err != nil {
|
||
return out, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// ToggleSavedDialogPin 翻转一个子会话的置顶状态;新置顶插到最前。
|
||
// 返回状态是否实际变化。
|
||
func (s *MessageStore) ToggleSavedDialogPin(ctx context.Context, userID int64, peer domain.Peer, pinned bool) (bool, error) {
|
||
if userID == 0 || peer.Type == "" || peer.ID == 0 {
|
||
return false, fmt.Errorf("toggle saved dialog pin: invalid input")
|
||
}
|
||
if !pinned {
|
||
rows, err := s.q.DeleteSavedDialogPin(ctx, sqlcgen.DeleteSavedDialogPinParams{
|
||
UserID: userID,
|
||
PeerType: string(peer.Type),
|
||
PeerID: peer.ID,
|
||
})
|
||
if err != nil {
|
||
return false, fmt.Errorf("delete saved dialog pin: %w", err)
|
||
}
|
||
return rows > 0, nil
|
||
}
|
||
count, err := s.q.CountSavedDialogPins(ctx, userID)
|
||
if err != nil {
|
||
return false, fmt.Errorf("count saved dialog pins: %w", err)
|
||
}
|
||
if int(count) >= domain.MaxPinnedSavedDialogs {
|
||
return false, domain.ErrPinnedSavedDialogsTooMuch
|
||
}
|
||
rows, err := s.q.UpsertSavedDialogPinFront(ctx, sqlcgen.UpsertSavedDialogPinFrontParams{
|
||
UserID: userID,
|
||
PeerType: string(peer.Type),
|
||
PeerID: peer.ID,
|
||
})
|
||
if err != nil {
|
||
return false, fmt.Errorf("upsert saved dialog pin: %w", err)
|
||
}
|
||
return rows > 0, nil
|
||
}
|
||
|
||
// ReorderPinnedSavedDialogs 全量重排置顶顺序;force 时清掉不在 order 中的
|
||
// 既有置顶(与 messages.reorderPinnedDialogs 的 force 语义一致)。
|
||
func (s *MessageStore) ReorderPinnedSavedDialogs(ctx context.Context, userID int64, order []domain.Peer, force bool) error {
|
||
if userID == 0 {
|
||
return fmt.Errorf("reorder pinned saved dialogs: missing user id")
|
||
}
|
||
if len(order) > domain.MaxPinnedSavedDialogs {
|
||
return domain.ErrPinnedSavedDialogsTooMuch
|
||
}
|
||
peerTypes := make([]string, 0, len(order))
|
||
peerIDs := make([]int64, 0, len(order))
|
||
for _, peer := range order {
|
||
if peer.Type == "" || peer.ID == 0 {
|
||
continue
|
||
}
|
||
peerTypes = append(peerTypes, string(peer.Type))
|
||
peerIDs = append(peerIDs, peer.ID)
|
||
}
|
||
if force {
|
||
if err := s.q.ClearSavedDialogPinsNotInOrder(ctx, sqlcgen.ClearSavedDialogPinsNotInOrderParams{
|
||
UserID: userID,
|
||
PeerTypes: peerTypes,
|
||
PeerIds: peerIDs,
|
||
}); err != nil {
|
||
return fmt.Errorf("clear saved dialog pins not in order: %w", err)
|
||
}
|
||
}
|
||
if len(peerTypes) == 0 {
|
||
return nil
|
||
}
|
||
if err := s.q.ReorderSavedDialogPins(ctx, sqlcgen.ReorderSavedDialogPinsParams{
|
||
UserID: userID,
|
||
PeerTypes: peerTypes,
|
||
PeerIds: peerIDs,
|
||
}); err != nil {
|
||
return fmt.Errorf("reorder saved dialog pins: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// DeleteSavedHistory 删除 self-chat 中一个 saved 子会话的消息(单批,
|
||
// 上限 MaxDeleteHistoryBatch),生成带 pts 的 delete_messages durable 事件,
|
||
// 修复 self dialog top;子会话删空时顺带清掉它的置顶行。
|
||
func (s *MessageStore) DeleteSavedHistory(ctx context.Context, req domain.DeleteSavedHistoryRequest) (domain.DeleteSavedHistoryResult, error) {
|
||
res := domain.DeleteSavedHistoryResult{}
|
||
if req.OwnerUserID == 0 || req.SavedPeer.Type == "" || req.SavedPeer.ID == 0 {
|
||
return res, fmt.Errorf("delete saved history: invalid input")
|
||
}
|
||
if req.Date == 0 {
|
||
req.Date = int(time.Now().Unix())
|
||
}
|
||
beginner, ok := s.db.(txBeginner)
|
||
if !ok {
|
||
return res, fmt.Errorf("delete saved history: db does not support transactions")
|
||
}
|
||
tx, err := beginner.Begin(ctx)
|
||
if err != nil {
|
||
return res, fmt.Errorf("begin delete saved history tx: %w", err)
|
||
}
|
||
qtx := sqlcgen.New(tx)
|
||
committed := false
|
||
defer func() {
|
||
if committed {
|
||
return
|
||
}
|
||
_ = tx.Rollback(ctx)
|
||
}()
|
||
if err := lockUsersForUpdate(ctx, tx, req.OwnerUserID); err != nil {
|
||
return res, fmt.Errorf("lock delete saved history user: %w", err)
|
||
}
|
||
rows, err := qtx.DeleteMessageBoxesBySavedPeerBatch(ctx, sqlcgen.DeleteMessageBoxesBySavedPeerBatchParams{
|
||
OwnerUserID: req.OwnerUserID,
|
||
SavedPeerType: string(req.SavedPeer.Type),
|
||
SavedPeerID: req.SavedPeer.ID,
|
||
MaxID: pgInt32NonNegative(req.MaxID),
|
||
MinDate: pgInt32NonNegative(req.MinDate),
|
||
MaxDate: pgInt32NonNegative(req.MaxDate),
|
||
LimitCount: int32(domain.MaxDeleteHistoryBatch),
|
||
})
|
||
if err != nil {
|
||
return res, fmt.Errorf("delete saved history batch: %w", err)
|
||
}
|
||
deleted := make([]deletedBox, 0, len(rows))
|
||
for _, row := range rows {
|
||
deleted = append(deleted, deletedBox{
|
||
ownerUserID: row.OwnerUserID,
|
||
boxID: int(row.BoxID),
|
||
privateMessageID: row.PrivateMessageID,
|
||
messageSenderID: row.MessageSenderID,
|
||
peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
|
||
})
|
||
}
|
||
delRes, err := s.finishDeleteMessagesTx(ctx, tx, qtx, req.OwnerUserID, req.OriginAuthKeyID, req.OriginSessionID, req.Date, deleted, nil)
|
||
if err != nil {
|
||
return res, err
|
||
}
|
||
if len(deleted) > 0 {
|
||
more, err := qtx.HasDeletableMessageBoxBySavedPeer(ctx, sqlcgen.HasDeletableMessageBoxBySavedPeerParams{
|
||
OwnerUserID: req.OwnerUserID,
|
||
SavedPeerType: string(req.SavedPeer.Type),
|
||
SavedPeerID: req.SavedPeer.ID,
|
||
MaxID: pgInt32NonNegative(req.MaxID),
|
||
MinDate: pgInt32NonNegative(req.MinDate),
|
||
MaxDate: pgInt32NonNegative(req.MaxDate),
|
||
})
|
||
if err != nil {
|
||
return res, fmt.Errorf("probe deletable saved history: %w", err)
|
||
}
|
||
res.More = more
|
||
if !more {
|
||
// 边界口径外可能还有残留(如 max_id 截断),子会话仍存活时
|
||
// 不能清置顶;只有全量删空才清。
|
||
alive, err := qtx.HasDeletableMessageBoxBySavedPeer(ctx, sqlcgen.HasDeletableMessageBoxBySavedPeerParams{
|
||
OwnerUserID: req.OwnerUserID,
|
||
SavedPeerType: string(req.SavedPeer.Type),
|
||
SavedPeerID: req.SavedPeer.ID,
|
||
})
|
||
if err != nil {
|
||
return res, fmt.Errorf("probe saved dialog alive: %w", err)
|
||
}
|
||
if !alive {
|
||
if _, err := qtx.DeleteSavedDialogPin(ctx, sqlcgen.DeleteSavedDialogPinParams{
|
||
UserID: req.OwnerUserID,
|
||
PeerType: string(req.SavedPeer.Type),
|
||
PeerID: req.SavedPeer.ID,
|
||
}); err != nil {
|
||
return res, fmt.Errorf("clear pin after saved history delete: %w", err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if err := tx.Commit(ctx); err != nil {
|
||
return res, fmt.Errorf("commit delete saved history tx: %w", err)
|
||
}
|
||
committed = true
|
||
for _, d := range delRes.Deleted {
|
||
if d.UserID == req.OwnerUserID {
|
||
res.MessageIDs = d.MessageIDs
|
||
res.Event = d.Event
|
||
}
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
// savedDialogTopRowFields 是三个 saved dialog top 查询共有的行字段。
|
||
type savedDialogTopRowFields struct {
|
||
BoxID int32
|
||
PrivateMessageID int64
|
||
OwnerUserID int64
|
||
PeerType string
|
||
PeerID int64
|
||
FromUserID int64
|
||
MessageDate int32
|
||
TtlPeriod int32
|
||
ExpiresAt int32
|
||
EditDate int32
|
||
HideEdited bool
|
||
Outgoing bool
|
||
Body string
|
||
EntitiesJson string
|
||
Silent bool
|
||
Noforwards bool
|
||
ReplyToMsgID int32
|
||
ReplyToPeerType string
|
||
ReplyToPeerID int64
|
||
ReplyToTopID int32
|
||
ReplyToStoryID int32
|
||
QuoteText string
|
||
QuoteEntitiesJson string
|
||
QuoteOffset int32
|
||
FwdFromPeerType string
|
||
FwdFromPeerID int64
|
||
FwdFromName string
|
||
FwdDate int32
|
||
FwdSavedFromPeerType string
|
||
FwdSavedFromPeerID int64
|
||
FwdSavedFromMsgID int32
|
||
SavedPeerType string
|
||
SavedPeerID int64
|
||
Pts int32
|
||
MediaJson string
|
||
MediaUnread bool
|
||
ReactionUnread bool
|
||
ViaBotID int64
|
||
GroupedID int64
|
||
Effect int64
|
||
ReplyMarkupJson string
|
||
RichMessageJson string
|
||
Pinned bool
|
||
}
|
||
|
||
func savedDialogRowFields[T sqlcgen.ListSavedDialogTopsRow | sqlcgen.ListPinnedSavedDialogTopsRow | sqlcgen.ListSavedDialogTopsByPeersRow](row T) savedDialogTopRowFields {
|
||
switch r := any(row).(type) {
|
||
case sqlcgen.ListSavedDialogTopsRow:
|
||
return savedDialogTopRowFields{r.BoxID, r.PrivateMessageID, r.OwnerUserID, r.PeerType, r.PeerID, r.FromUserID, r.MessageDate, r.TtlPeriod, r.ExpiresAt, r.EditDate, r.HideEdited, r.Outgoing, r.Body, r.EntitiesJson, r.Silent, r.Noforwards, r.ReplyToMsgID, r.ReplyToPeerType, r.ReplyToPeerID, r.ReplyToTopID, r.ReplyToStoryID, r.QuoteText, r.QuoteEntitiesJson, r.QuoteOffset, r.FwdFromPeerType, r.FwdFromPeerID, r.FwdFromName, r.FwdDate, r.FwdSavedFromPeerType, r.FwdSavedFromPeerID, r.FwdSavedFromMsgID, r.SavedPeerType, r.SavedPeerID, r.Pts, r.MediaJson, r.MediaUnread, r.ReactionUnread, r.ViaBotID, r.GroupedID, r.Effect, r.ReplyMarkupJson, r.RichMessageJson, r.Pinned}
|
||
case sqlcgen.ListPinnedSavedDialogTopsRow:
|
||
return savedDialogTopRowFields{r.BoxID, r.PrivateMessageID, r.OwnerUserID, r.PeerType, r.PeerID, r.FromUserID, r.MessageDate, r.TtlPeriod, r.ExpiresAt, r.EditDate, r.HideEdited, r.Outgoing, r.Body, r.EntitiesJson, r.Silent, r.Noforwards, r.ReplyToMsgID, r.ReplyToPeerType, r.ReplyToPeerID, r.ReplyToTopID, r.ReplyToStoryID, r.QuoteText, r.QuoteEntitiesJson, r.QuoteOffset, r.FwdFromPeerType, r.FwdFromPeerID, r.FwdFromName, r.FwdDate, r.FwdSavedFromPeerType, r.FwdSavedFromPeerID, r.FwdSavedFromMsgID, r.SavedPeerType, r.SavedPeerID, r.Pts, r.MediaJson, r.MediaUnread, r.ReactionUnread, r.ViaBotID, r.GroupedID, r.Effect, r.ReplyMarkupJson, r.RichMessageJson, r.Pinned}
|
||
case sqlcgen.ListSavedDialogTopsByPeersRow:
|
||
return savedDialogTopRowFields{r.BoxID, r.PrivateMessageID, r.OwnerUserID, r.PeerType, r.PeerID, r.FromUserID, r.MessageDate, r.TtlPeriod, r.ExpiresAt, r.EditDate, r.HideEdited, r.Outgoing, r.Body, r.EntitiesJson, r.Silent, r.Noforwards, r.ReplyToMsgID, r.ReplyToPeerType, r.ReplyToPeerID, r.ReplyToTopID, r.ReplyToStoryID, r.QuoteText, r.QuoteEntitiesJson, r.QuoteOffset, r.FwdFromPeerType, r.FwdFromPeerID, r.FwdFromName, r.FwdDate, r.FwdSavedFromPeerType, r.FwdSavedFromPeerID, r.FwdSavedFromMsgID, r.SavedPeerType, r.SavedPeerID, r.Pts, r.MediaJson, r.MediaUnread, r.ReactionUnread, r.ViaBotID, r.GroupedID, r.Effect, r.ReplyMarkupJson, r.RichMessageJson, r.Pinned}
|
||
}
|
||
return savedDialogTopRowFields{}
|
||
}
|
||
|
||
func messageFromSavedDialogRow(row savedDialogTopRowFields) (domain.Message, error) {
|
||
entities, err := decodeMessageEntities(row.EntitiesJson)
|
||
if err != nil {
|
||
return domain.Message{}, fmt.Errorf("decode saved dialog message entities: %w", err)
|
||
}
|
||
silent, noforwards, reply, forward, err := messageMetadataFromFields(
|
||
row.Silent,
|
||
row.Noforwards,
|
||
row.ReplyToMsgID,
|
||
row.ReplyToPeerType,
|
||
row.ReplyToPeerID,
|
||
row.ReplyToTopID,
|
||
row.ReplyToStoryID,
|
||
row.QuoteText,
|
||
row.QuoteEntitiesJson,
|
||
row.QuoteOffset,
|
||
row.FwdFromPeerType,
|
||
row.FwdFromPeerID,
|
||
row.FwdFromName,
|
||
row.FwdDate,
|
||
row.FwdSavedFromPeerType,
|
||
row.FwdSavedFromPeerID,
|
||
row.FwdSavedFromMsgID,
|
||
)
|
||
if err != nil {
|
||
return domain.Message{}, fmt.Errorf("decode saved dialog message metadata: %w", err)
|
||
}
|
||
media, err := decodeMessageMedia(row.MediaJson)
|
||
if err != nil {
|
||
return domain.Message{}, fmt.Errorf("decode saved dialog message media: %w", err)
|
||
}
|
||
markup, err := decodeReplyMarkup(row.ReplyMarkupJson)
|
||
if err != nil {
|
||
return domain.Message{}, fmt.Errorf("decode saved dialog message reply markup: %w", err)
|
||
}
|
||
rich, err := decodeRichMessage(row.RichMessageJson)
|
||
if err != nil {
|
||
return domain.Message{}, fmt.Errorf("decode saved dialog message rich message: %w", err)
|
||
}
|
||
return 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),
|
||
HideEdited: row.HideEdited,
|
||
Out: row.Outgoing,
|
||
Silent: silent,
|
||
NoForwards: noforwards,
|
||
Body: row.Body,
|
||
Entities: entities,
|
||
ReplyTo: reply,
|
||
Forward: forward,
|
||
Pts: int(row.Pts),
|
||
TTLPeriod: int(row.TtlPeriod),
|
||
ExpiresAt: int(row.ExpiresAt),
|
||
Media: media,
|
||
MediaUnread: row.MediaUnread,
|
||
ReactionUnread: row.ReactionUnread,
|
||
ViaBotID: row.ViaBotID,
|
||
GroupedID: row.GroupedID,
|
||
Effect: row.Effect,
|
||
ReplyMarkup: markup,
|
||
RichMessage: rich,
|
||
Pinned: row.Pinned,
|
||
SavedPeer: savedPeerFromFields(row.SavedPeerType, row.SavedPeerID),
|
||
}, nil
|
||
}
|