chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,650 @@
package postgres
import (
"context"
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"sort"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
"time"
)
func (s *MessageStore) Create(ctx context.Context, msg domain.Message) (domain.Message, error) {
if err := s.ensureOfficialSystemUser(ctx, msg); err != nil {
return domain.Message{}, err
}
entities, err := encodeMessageEntities(msg.Entities)
if err != nil {
return domain.Message{}, err
}
if msg.Date == 0 {
msg.Date = int(time.Now().Unix())
}
if msg.ID == 0 {
msg.ID, err = s.boxIDs.NextBoxID(ctx, msg.OwnerUserID)
if err != nil {
return domain.Message{}, fmt.Errorf("allocate login message box id: %w", err)
}
}
row, err := s.q.CreateMessage(ctx, sqlcgen.CreateMessageParams{
OwnerUserID: msg.OwnerUserID,
BoxID: int32(msg.ID),
PeerType: string(msg.Peer.Type),
PeerID: msg.Peer.ID,
FromUserID: msg.From.ID,
MessageDate: int32(msg.Date),
Outgoing: msg.Out,
Body: msg.Body,
EntitiesJson: entities,
Pts: int32(msg.Pts),
})
if err != nil {
return domain.Message{}, fmt.Errorf("create message: %w", err)
}
return messageFromCreateRow(row)
}
func (s *MessageStore) ensureOfficialSystemUser(ctx context.Context, msg domain.Message) error {
if msg.Peer.Type != domain.PeerTypeUser && msg.From.Type != domain.PeerTypeUser {
return nil
}
u, ok := domain.SystemUserByID(msg.Peer.ID)
if !ok {
u, ok = domain.SystemUserByID(msg.From.ID)
}
if !ok {
return nil
}
if _, err := s.db.Exec(ctx, `
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support, about, is_bot, bot_info_version)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (id) DO UPDATE SET
access_hash = EXCLUDED.access_hash,
phone = EXCLUDED.phone,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
username = EXCLUDED.username,
country_code = EXCLUDED.country_code,
verified = EXCLUDED.verified,
support = EXCLUDED.support,
about = EXCLUDED.about,
is_bot = EXCLUDED.is_bot,
bot_info_version = EXCLUDED.bot_info_version,
updated_at = now()
`, u.ID, u.AccessHash, u.Phone, u.FirstName, u.LastName, u.Username, u.CountryCode, u.Verified, u.Support, u.About, u.Bot, u.BotInfoVersion); err != nil {
return fmt.Errorf("ensure official system user: %w", err)
}
return nil
}
func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPrivateTextRequest) (res domain.SendPrivateTextResult, err error) {
for attempt := 0; attempt < 2; attempt++ {
res, err = s.sendPrivateTextOnce(ctx, req)
if err == nil {
return res, nil
}
if !isMessageBoxDuplicateKey(err) || attempt > 0 {
return domain.SendPrivateTextResult{}, err
}
if recoverErr := s.bumpBoxIDCountersAfterDuplicate(ctx, req); recoverErr != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("%w; recover box id counters: %v", err, recoverErr)
}
}
return domain.SendPrivateTextResult{}, err
}
func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendPrivateTextRequest) (res domain.SendPrivateTextResult, err error) {
if req.SenderUserID == 0 || req.RecipientUserID == 0 {
return domain.SendPrivateTextResult{}, fmt.Errorf("send private text: missing user id")
}
if req.RandomID == 0 {
return domain.SendPrivateTextResult{}, fmt.Errorf("send private text: missing random id")
}
if req.Message == "" && req.Media.IsZero() {
return domain.SendPrivateTextResult{}, fmt.Errorf("send private text: empty message")
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
entities, err := encodeMessageEntities(req.Entities)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
mediaJSON, err := encodeMessageMedia(req.Media)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
// reply_markupbot inline keyboard随消息一并入双盒普通用户发送恒 nil → "{}"。
replyMarkupJSON, err := encodeReplyMarkup(req.ReplyMarkup)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
// rich_messageLayer 227 富文本)随消息一并入双盒;普通消息恒 nil → "{}"。
richMessageJSON, err := encodeRichMessage(req.RichMessage)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
senderReply, recipientReply, err := s.resolvePrivateSendReply(ctx, req)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
senderMeta, err := messageMetadataParamsFrom(req.Silent, req.NoForwards, senderReply, req.Forward)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
recipientMeta, err := messageMetadataParamsFrom(req.Silent, req.NoForwards, recipientReply, req.Forward)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.SendPrivateTextResult{}, fmt.Errorf("send private text: db does not support transactions")
}
var recipientBoxID, recipientPts int
selfMessage := req.RecipientUserID == req.SenderUserID
deliverRecipient := !selfMessage && !req.RecipientBlocked
if selfMessage {
savedPeer := domain.SavedPeerForSelfChat(req.SenderUserID, req.Forward)
senderMeta.SavedPeerType = string(savedPeer.Type)
senderMeta.SavedPeerID = savedPeer.ID
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("begin send message tx: %w", err)
}
qtx := sqlcgen.New(tx)
committed := false
defer func() {
if committed {
return
}
_ = tx.Rollback(ctx)
}()
// 事务级 advisory lock 串行化涉及收发双方的并发写,在任何行锁之前获取,消除 watermark/dialog
// 行锁的 AB-BA 死锁A↔B 反向并发 send/read/edit
if err := lockUsersForUpdate(ctx, tx, req.SenderUserID, req.RecipientUserID); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("lock send users: %w", err)
}
ttlPeriod := req.TTLPeriod
if ttlPeriod == 0 {
ttlPeriod, err = privateHistoryTTLPeriod(ctx, tx, req.SenderUserID, req.RecipientUserID)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("load private ttl: %w", err)
}
}
expiresAt := 0
if ttlPeriod > 0 {
expiresAt = req.Date + ttlPeriod
}
privateArg := sqlcgen.CreatePrivateMessageParams{
SenderUserID: req.SenderUserID,
RecipientUserID: req.RecipientUserID,
RandomID: req.RandomID,
MessageDate: int32(req.Date),
Body: req.Message,
TtlPeriod: int32(ttlPeriod),
ExpiresAt: int32(expiresAt),
EntitiesJson: entities,
MediaJson: mediaJSON,
ReplyMarkupJson: replyMarkupJSON,
RichMessageJson: richMessageJSON,
ViaBotID: req.ViaBotID,
GroupedID: req.GroupedID,
Effect: req.Effect,
}
applyCreatePrivateMessageMetadata(&privateArg, senderMeta)
pm, err := qtx.CreatePrivateMessage(ctx, privateArg)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
// 幂等重复:返回原消息盒;此时还没有分配 pts重复发送不应制造额外事件。
dup, dupErr := s.duplicateSendResult(ctx, req.SenderUserID, req.RecipientUserID, req.RandomID)
if dupErr != nil {
return domain.SendPrivateTextResult{}, dupErr
}
dup.Duplicate = true
return dup, nil
}
return domain.SendPrivateTextResult{}, fmt.Errorf("create private message: %w", err)
}
senderBoxID, err := s.boxIDs.NextBoxID(ctx, req.SenderUserID)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("allocate sender box id: %w", err)
}
senderPts, err := s.reservePts(ctx, tx, req.SenderUserID)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("allocate sender pts: %w", err)
}
if deliverRecipient {
recipientBoxID, err = s.boxIDs.NextBoxID(ctx, req.RecipientUserID)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("allocate recipient box id: %w", err)
}
recipientPts, err = s.reservePts(ctx, tx, req.RecipientUserID)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("allocate recipient pts: %w", err)
}
}
senderArg := sqlcgen.CreateMessageBoxParams{
OwnerUserID: req.SenderUserID,
BoxID: int32(senderBoxID),
PrivateMessageID: pm.ID,
MessageSenderID: req.SenderUserID,
PeerType: string(domain.PeerTypeUser),
PeerID: req.RecipientUserID,
FromUserID: req.SenderUserID,
MessageDate: int32(req.Date),
Outgoing: true,
Body: req.Message,
TtlPeriod: int32(ttlPeriod),
ExpiresAt: int32(expiresAt),
EntitiesJson: entities,
Pts: int32(senderPts),
MediaJson: mediaJSON,
ReplyMarkupJson: replyMarkupJSON,
RichMessageJson: richMessageJSON,
ViaBotID: req.ViaBotID,
GroupedID: req.GroupedID,
Effect: req.Effect,
// voice/round 在发送者自己的副本上也保持"未听",直到对端
// readMessageContents 触发 sender 侧清除;发给自己无人可听,恒已读。
MediaUnread: req.Media.HasUnreadPayload() && !selfMessage,
ReactionUnread: false,
}
applyCreateMessageBoxMetadata(&senderArg, senderMeta)
senderRow, err := qtx.CreateMessageBox(ctx, senderArg)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("create sender box: %w", err)
}
sender := messageFromBoxRow(senderRow)
// 共享媒体索引(0118):发送者侧 box 按媒体类别建索引(peer=收件人)。
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.SenderUserID, req.RecipientUserID, int(senderBoxID), req.Date, req.Media, req.Entities); err != nil {
return domain.SendPrivateTextResult{}, err
}
if err := qtx.UpsertOutboxDialog(ctx, sqlcgen.UpsertOutboxDialogParams{
UserID: req.SenderUserID,
PeerType: string(domain.PeerTypeUser),
PeerID: req.RecipientUserID,
TopMessageID: int32(sender.ID),
TopMessageDate: int32(sender.Date),
}); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("upsert sender dialog: %w", err)
}
if err := appendNewMessageEvent(ctx, qtx, sender); err != nil {
return domain.SendPrivateTextResult{}, err
}
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
TargetUserID: req.SenderUserID,
Pts: int32(senderPts),
EventType: string(domain.UpdateEventNewMessage),
ExcludeAuthKeyID: authKeyIDToInt64(req.OriginAuthKeyID),
ExcludeSessionID: req.OriginSessionID,
}); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("enqueue sender dispatch: %w", err)
}
recipient := domain.Message{}
if selfMessage {
recipient = sender
}
if deliverRecipient {
recipientArg := sqlcgen.CreateMessageBoxParams{
OwnerUserID: req.RecipientUserID,
BoxID: int32(recipientBoxID),
PrivateMessageID: pm.ID,
MessageSenderID: req.SenderUserID,
PeerType: string(domain.PeerTypeUser),
PeerID: req.SenderUserID,
FromUserID: req.SenderUserID,
MessageDate: int32(req.Date),
Outgoing: false,
Body: req.Message,
TtlPeriod: int32(ttlPeriod),
ExpiresAt: int32(expiresAt),
EntitiesJson: entities,
Pts: int32(recipientPts),
MediaJson: mediaJSON,
ReplyMarkupJson: replyMarkupJSON,
RichMessageJson: richMessageJSON,
ViaBotID: req.ViaBotID,
GroupedID: req.GroupedID,
Effect: req.Effect,
MediaUnread: req.Media.HasUnreadPayload(),
ReactionUnread: false,
}
applyCreateMessageBoxMetadata(&recipientArg, recipientMeta)
recipientRow, err := qtx.CreateMessageBox(ctx, recipientArg)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("create recipient box: %w", err)
}
recipient = messageFromBoxRow(recipientRow)
// 共享媒体索引(0118):收件人侧 box 按媒体类别建索引(peer=发送者)。
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.RecipientUserID, req.SenderUserID, int(recipientBoxID), req.Date, req.Media, req.Entities); err != nil {
return domain.SendPrivateTextResult{}, err
}
if err := qtx.UpsertInboxDialog(ctx, sqlcgen.UpsertInboxDialogParams{
UserID: req.RecipientUserID,
PeerType: string(domain.PeerTypeUser),
PeerID: req.SenderUserID,
TopMessageID: int32(recipient.ID),
TopMessageDate: int32(recipient.Date),
}); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("upsert recipient dialog: %w", err)
}
if err := appendNewMessageEvent(ctx, qtx, recipient); err != nil {
return domain.SendPrivateTextResult{}, err
}
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
TargetUserID: req.RecipientUserID,
Pts: int32(recipientPts),
EventType: string(domain.UpdateEventNewMessage),
ExcludeAuthKeyID: 0,
ExcludeSessionID: 0,
}); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("enqueue recipient dispatch: %w", err)
}
}
if err := tx.Commit(ctx); err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("commit send message tx: %w", err)
}
committed = true
return domain.SendPrivateTextResult{
SenderMessage: sender,
RecipientMessage: recipient,
SenderEvent: eventFromMessage(sender),
RecipientEvent: eventFromMessage(recipient),
}, nil
}
type boxIDCounterBumper interface {
BumpBoxIDAtLeast(ctx context.Context, userID int64, floor int) error
}
func (s *MessageStore) bumpBoxIDCountersAfterDuplicate(ctx context.Context, req domain.SendPrivateTextRequest) error {
bumper, ok := s.boxIDs.(boxIDCounterBumper)
if !ok {
return nil
}
userIDs := []int64{req.SenderUserID}
if req.RecipientUserID != 0 && req.RecipientUserID != req.SenderUserID {
userIDs = append(userIDs, req.RecipientUserID)
}
for _, userID := range userIDs {
maxID, err := s.q.MaxMessageBoxID(ctx, userID)
if err != nil {
return fmt.Errorf("max message box id for %d: %w", userID, err)
}
if err := bumper.BumpBoxIDAtLeast(ctx, userID, int(maxID)); err != nil {
return fmt.Errorf("bump box id for %d: %w", userID, err)
}
}
return nil
}
func isMessageBoxDuplicateKey(err error) bool {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) {
return false
}
return pgErr.Code == "23505" && strings.Contains(pgErr.ConstraintName, "message_boxes")
}
func (s *MessageStore) duplicateSendResult(ctx context.Context, senderUserID, recipientUserID, randomID int64) (domain.SendPrivateTextResult, error) {
pm, err := s.q.GetPrivateMessageByRandomID(ctx, sqlcgen.GetPrivateMessageByRandomIDParams{
SenderUserID: senderUserID,
RandomID: randomID,
})
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("get duplicate private message: %w", err)
}
senderRow, err := s.q.GetMessageBoxByPrivateMessage(ctx, sqlcgen.GetMessageBoxByPrivateMessageParams{
OwnerUserID: senderUserID,
PrivateMessageID: pm.ID,
})
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("get duplicate sender box: %w", err)
}
sender := messageFromGetBoxRow(senderRow)
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)
}
return domain.SendPrivateTextResult{
SenderMessage: sender,
RecipientMessage: recipient,
SenderEvent: eventFromMessage(sender),
RecipientEvent: eventFromMessage(recipient),
}, nil
}
func (s *MessageStore) resolvePrivateSendReply(ctx context.Context, req domain.SendPrivateTextRequest) (*domain.MessageReply, *domain.MessageReply, error) {
if req.ReplyTo == nil {
return nil, nil, nil
}
if req.ReplyTo.StoryID > 0 {
// story 回复评论无源消息可查story 作者就是会话对端recipient双盒同持。
if req.ReplyTo.StoryID > domain.MaxStoryID {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
reply := &domain.MessageReply{
StoryID: req.ReplyTo.StoryID,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID},
}
return cloneMessageReply(reply), cloneMessageReply(reply), nil
}
if req.ReplyTo.MessageID <= 0 || req.ReplyTo.MessageID > domain.MaxMessageBoxID {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
peer := req.ReplyTo.Peer
if peer.ID == 0 {
peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID}
}
if peer.Type != domain.PeerTypeUser || peer.ID != req.RecipientUserID {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
source, err := s.q.GetMessageBoxForReply(ctx, sqlcgen.GetMessageBoxForReplyParams{
OwnerUserID: req.SenderUserID,
PeerType: string(peer.Type),
PeerID: peer.ID,
BoxID: int32(req.ReplyTo.MessageID),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
return nil, nil, fmt.Errorf("get reply message: %w", err)
}
senderReply := cloneMessageReply(req.ReplyTo)
senderReply.MessageID = int(source.BoxID)
senderReply.Peer = peer
if req.SenderUserID == req.RecipientUserID {
return senderReply, cloneMessageReply(senderReply), nil
}
recipientRow, err := s.q.GetMessageBoxByPrivateMessage(ctx, sqlcgen.GetMessageBoxByPrivateMessageParams{
OwnerUserID: req.RecipientUserID,
PrivateMessageID: source.PrivateMessageID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return senderReply, nil, nil
}
return nil, nil, fmt.Errorf("get recipient reply message: %w", err)
}
recipientReply := cloneMessageReply(senderReply)
recipientReply.MessageID = int(recipientRow.BoxID)
recipientReply.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
return senderReply, recipientReply, nil
}
func appendNewMessageEvent(ctx context.Context, q *sqlcgen.Queries, msg domain.Message) error {
boxID := int32(msg.ID)
peerType := string(msg.Peer.Type)
peerID := msg.Peer.ID
if err := q.AppendUserUpdateEvent(ctx, sqlcgen.AppendUserUpdateEventParams{
UserID: msg.OwnerUserID,
Pts: int32(msg.Pts),
PtsCount: 1,
Date: int32(msg.Date),
EventType: string(domain.UpdateEventNewMessage),
EventPeers: []byte("[]"),
PeerSettings: []byte("{}"),
MessageIds: []byte("[]"),
DialogFilter: []byte("{}"),
FilterOrder: []byte("[]"),
FolderPeers: []byte("[]"),
StoryPayload: []byte("{}"),
ReactionPayload: []byte("{}"),
MessageBoxID: &boxID,
PeerType: &peerType,
PeerID: &peerID,
}); err != nil {
return fmt.Errorf("append new message event: %w", err)
}
return nil
}
// lockUsersForUpdate 在事务开始处用事务级 advisory lock 串行化所有涉及指定用户的并发写事务。
// advisory lock 与行锁处于独立锁空间,且按 user_id 升序获取,因此:① 不会与后续 dialog /
// watermark / box 行锁交叉成跨类型死锁;② 任意两个共享某用户的写事务send/read/edit/delete 对
// 收发双方的并发操作)被完全串行化,从根上消除它们在 watermark 与 dialog 行上因加锁顺序相反
// 导致的 AB-BA 死锁——既包含本次 watermark 优化新引入的user_update_watermarks FOR UPDATE
// 也包含 dialog upsert 既有的反向行锁。advisory xact lock 在事务结束自动释放;同对用户本就竞争
// 这些行(天然串行),不额外降并发,不同用户集合的事务仍并行。**必须在任何行锁之前调用。**
func lockUsersForUpdate(ctx context.Context, tx pgx.Tx, userIDs ...int64) error {
if len(userIDs) == 0 {
return nil
}
unique := make([]int64, 0, len(userIDs))
seen := make(map[int64]struct{}, len(userIDs))
for _, id := range userIDs {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
unique = append(unique, id)
}
sort.Slice(unique, func(i, j int) bool { return unique[i] < unique[j] })
for _, id := range unique {
if _, err := tx.Exec(ctx, "SELECT pg_advisory_xact_lock($1)", id); err != nil {
return fmt.Errorf("advisory lock user %d: %w", id, err)
}
}
return nil
}
func applyCreatePrivateMessageMetadata(arg *sqlcgen.CreatePrivateMessageParams, meta messageMetadataParams) {
arg.Silent = meta.Silent
arg.Noforwards = meta.Noforwards
arg.ReplyToMsgID = meta.ReplyToMsgID
arg.ReplyToPeerType = meta.ReplyToPeerType
arg.ReplyToPeerID = meta.ReplyToPeerID
arg.ReplyToTopID = meta.ReplyToTopID
arg.ReplyToStoryID = meta.ReplyToStoryID
arg.QuoteText = meta.QuoteText
arg.QuoteEntitiesJson = meta.QuoteEntitiesJSON
arg.QuoteOffset = meta.QuoteOffset
arg.FwdFromPeerType = meta.FwdFromPeerType
arg.FwdFromPeerID = meta.FwdFromPeerID
arg.FwdFromName = meta.FwdFromName
arg.FwdDate = meta.FwdDate
}
func applyCreateMessageBoxMetadata(arg *sqlcgen.CreateMessageBoxParams, meta messageMetadataParams) {
arg.Silent = meta.Silent
arg.Noforwards = meta.Noforwards
arg.ReplyToMsgID = meta.ReplyToMsgID
arg.ReplyToPeerType = meta.ReplyToPeerType
arg.ReplyToPeerID = meta.ReplyToPeerID
arg.ReplyToTopID = meta.ReplyToTopID
arg.ReplyToStoryID = meta.ReplyToStoryID
arg.QuoteText = meta.QuoteText
arg.QuoteEntitiesJson = meta.QuoteEntitiesJSON
arg.QuoteOffset = meta.QuoteOffset
arg.FwdFromPeerType = meta.FwdFromPeerType
arg.FwdFromPeerID = meta.FwdFromPeerID
arg.FwdFromName = meta.FwdFromName
arg.FwdDate = meta.FwdDate
arg.FwdSavedFromPeerType = meta.FwdSavedFromPeerType
arg.FwdSavedFromPeerID = meta.FwdSavedFromPeerID
arg.FwdSavedFromMsgID = meta.FwdSavedFromMsgID
arg.SavedPeerType = meta.SavedPeerType
arg.SavedPeerID = meta.SavedPeerID
}
func privateHistoryTTLPeriod(ctx context.Context, db sqlcgen.DBTX, ownerUserID, peerUserID int64) (int, error) {
if ownerUserID == 0 || peerUserID == 0 {
return 0, nil
}
var period int
err := db.QueryRow(ctx, `
SELECT COALESCE(NULLIF(d.ttl_period, 0), u.default_history_ttl_period, 0)::int
FROM users u
LEFT JOIN dialogs d
ON d.user_id = u.id
AND d.peer_type = 'user'
AND d.peer_id = $2
WHERE u.id = $1
`, ownerUserID, peerUserID).Scan(&period)
if errors.Is(err, pgx.ErrNoRows) {
return 0, nil
}
if err != nil {
return 0, err
}
if period < 0 {
return 0, nil
}
return period, nil
}
func messageFromCreateRow(row sqlcgen.CreateMessageRow) (domain.Message, error) {
entities, err := decodeMessageEntities(row.EntitiesJson)
if err != nil {
return domain.Message{}, fmt.Errorf("decode message entities: %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),
Out: row.Outgoing,
Body: row.Body,
Entities: entities,
Pts: int(row.Pts),
}, nil
}