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,273 @@
package postgres
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
"time"
)
func (s *MessageStore) EditMessage(ctx context.Context, req domain.EditMessageRequest) (res domain.EditMessageResult, err error) {
res = domain.EditMessageResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 {
return res, fmt.Errorf("edit message: missing owner user id")
}
if req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return res, domain.ErrMessageIDInvalid
}
if req.EditDate == 0 {
req.EditDate = int(time.Now().Unix())
}
entities, err := encodeMessageEntities(req.Entities)
if err != nil {
return res, err
}
beginner, ok := s.db.(txBeginner)
if !ok {
return res, fmt.Errorf("edit message: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return res, fmt.Errorf("begin edit message tx: %w", err)
}
qtx := sqlcgen.New(tx)
committed := false
defer func() {
if committed {
return
}
_ = tx.Rollback(ctx)
}()
// advisory lock 串行化与会话对端的并发写,须在行锁前获取,消除 AB-BA 死锁。
if err := lockUsersForUpdate(ctx, tx, req.OwnerUserID, req.Peer.ID); err != nil {
return res, fmt.Errorf("lock edit message users: %w", err)
}
target, err := qtx.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{
OwnerUserID: req.OwnerUserID,
BoxID: int32(req.ID),
PeerType: string(req.Peer.Type),
PeerID: req.Peer.ID,
})
// 空文本只在目标消息携带媒体(或本次写入媒体)时合法(清空 caption
// 纯文本消息清空会留下既无 body 也无 media 的空壳。
if err == nil && req.Message == "" && req.Media == nil && (target.MediaJson == "" || target.MediaJson == "{}") {
return res, domain.ErrMessageEmpty
}
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return res, domain.ErrMessageIDInvalid
}
return res, fmt.Errorf("get message for edit: %w", err)
}
oldEntities, err := decodeMessageEntities(target.EntitiesJson)
if err != nil {
return res, fmt.Errorf("decode target entities: %w", err)
}
authorEdit := target.Outgoing && target.MessageSenderID == req.OwnerUserID && target.FromUserID == req.OwnerUserID
viaBotEdit := req.ViaBotEditBotID != 0 && target.ViaBotID == req.ViaBotEditBotID
if !authorEdit && !viaBotEdit && !req.WebPageResolve && !validTodoParticipantEdit(req, target, oldEntities) {
return res, domain.ErrMessageAuthorRequired
}
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && sameMessageEntities(oldEntities, req.Entities) {
return res, domain.ErrMessageNotModified
}
replyMarkupJSON, err := encodeReplyMarkup(req.ReplyMarkup)
if err != nil {
return res, fmt.Errorf("encode edit reply markup: %w", err)
}
messageSenderID := target.MessageSenderID
boxes, err := qtx.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
OwnerUserIds: privateMessageOwnerIDs(req.OwnerUserID, req.Peer.ID),
MessageSenderID: messageSenderID,
PrivateMessageID: target.PrivateMessageID,
})
if err != nil {
return res, fmt.Errorf("list visible edit boxes: %w", err)
}
if len(boxes) == 0 {
return res, domain.ErrMessageIDInvalid
}
if req.WebPageResolve {
// 链接预览就地替换:仅换 media不碰 body/entities/edit_date故不标记「已编辑」
// 逐 box reserve 账号 pts + 追加 web_page 事件 + dispatch。幂等守卫见下。
targetMedia, err := decodeMessageMedia(target.MediaJson)
if err != nil {
return res, fmt.Errorf("decode target media for web page resolve: %w", err)
}
if !domain.IsPendingWebPageMedia(targetMedia, req.ExpectedWebPageID) {
return res, domain.ErrMessageNotModified
}
mediaJSON, err := encodeMessageMedia(req.Media)
if err != nil {
return res, fmt.Errorf("encode resolved web page media: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE private_messages SET media = $3
WHERE sender_user_id = $1 AND id = $2`, messageSenderID, target.PrivateMessageID, mediaJSON); err != nil {
return res, fmt.Errorf("update private message web page media: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE message_boxes SET media = $3
WHERE message_sender_id = $1 AND private_message_id = $2`, messageSenderID, target.PrivateMessageID, mediaJSON); err != nil {
return res, fmt.Errorf("update message box web page media: %w", err)
}
res.Edited = make([]domain.EditedMessageForUser, 0, len(boxes))
for _, box := range boxes {
pts, err := s.reservePts(ctx, tx, box.OwnerUserID)
if err != nil {
return res, fmt.Errorf("allocate web page resolve pts: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE message_boxes SET pts = $3
WHERE owner_user_id = $1 AND box_id = $2`, box.OwnerUserID, box.BoxID, int32(pts)); err != nil {
return res, fmt.Errorf("bump message box pts for web page: %w", err)
}
msg, err := messageFromVisibleBoxRow(box)
if err != nil {
return res, err
}
// box 行取自 media 替换前:用解析结果与新 pts 覆盖(链接预览不改文本/实体,
// 故共享媒体索引无需重建)。
msg.Media = req.Media
msg.Pts = pts
event := domain.UpdateEvent{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventWebPage,
Pts: pts,
PtsCount: 1,
Date: msg.Date,
Message: msg,
}
if err := appendUserUpdateEvent(ctx, tx, qtx, msg.OwnerUserID, event); err != nil {
return res, fmt.Errorf("append web page event: %w", err)
}
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
TargetUserID: msg.OwnerUserID,
Pts: int32(pts),
EventType: string(domain.UpdateEventWebPage),
ExcludeAuthKeyID: authKeyIDToInt64(req.OriginAuthKeyID),
ExcludeSessionID: req.OriginSessionID,
}); err != nil {
return res, fmt.Errorf("enqueue web page dispatch: %w", err)
}
res.Edited = append(res.Edited, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
}
if err := tx.Commit(ctx); err != nil {
return res, fmt.Errorf("commit web page resolve tx: %w", err)
}
committed = true
return res, nil
}
if err := qtx.UpdatePrivateMessageEdit(ctx, sqlcgen.UpdatePrivateMessageEditParams{
SenderUserID: messageSenderID,
PrivateMessageID: target.PrivateMessageID,
Body: req.Message,
EntitiesJson: entities,
EditDate: int32(req.EditDate),
SetReplyMarkup: req.SetReplyMarkup,
ReplyMarkupJson: replyMarkupJSON,
}); err != nil {
return res, fmt.Errorf("update private message edit: %w", err)
}
if req.Media != nil {
// 媒体快照整体替换live location 续报/停止);先于 box 行编辑执行,
// 让 UpdateMessageBoxEdit RETURNING 直接带回新媒体。
mediaJSON, err := encodeMessageMedia(req.Media)
if err != nil {
return res, fmt.Errorf("encode edit message media: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE private_messages SET media = $3
WHERE sender_user_id = $1 AND id = $2`, messageSenderID, target.PrivateMessageID, mediaJSON); err != nil {
return res, fmt.Errorf("update private message media: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE message_boxes SET media = $3
WHERE message_sender_id = $1 AND private_message_id = $2`, messageSenderID, target.PrivateMessageID, mediaJSON); err != nil {
return res, fmt.Errorf("update message box media: %w", err)
}
}
res.Edited = make([]domain.EditedMessageForUser, 0, len(boxes))
for _, box := range boxes {
pts, err := s.reservePts(ctx, tx, box.OwnerUserID)
if err != nil {
return res, fmt.Errorf("allocate edit message pts: %w", err)
}
updated, err := qtx.UpdateMessageBoxEdit(ctx, sqlcgen.UpdateMessageBoxEditParams{
OwnerUserID: box.OwnerUserID,
BoxID: box.BoxID,
Body: req.Message,
EntitiesJson: entities,
EditDate: int32(req.EditDate),
Pts: int32(pts),
SetReplyMarkup: req.SetReplyMarkup,
ReplyMarkupJson: replyMarkupJSON,
})
if err != nil {
return res, fmt.Errorf("update message box edit: %w", err)
}
msg, err := messageFromUpdateEditRow(updated)
if err != nil {
return res, err
}
// 共享媒体索引(0118):编辑可换媒体、也可改文本链接实体 → 按编辑后有效媒体+实体逐 box 重建。
if err := replaceMessageBoxMediaIndexTx(ctx, tx, msg.OwnerUserID, msg.Peer.ID, msg.ID, msg.Date, msg.Media, msg.Entities); err != nil {
return res, err
}
event := domain.UpdateEvent{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventEditMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: req.EditDate,
Message: msg,
}
if err := appendUserUpdateEvent(ctx, tx, qtx, msg.OwnerUserID, event); err != nil {
return res, fmt.Errorf("append edit message event: %w", err)
}
dispatchAuthKeyID := [8]byte{}
dispatchSessionID := int64(0)
if msg.OwnerUserID == req.OwnerUserID {
dispatchAuthKeyID = req.OriginAuthKeyID
dispatchSessionID = req.OriginSessionID
}
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
TargetUserID: msg.OwnerUserID,
Pts: int32(pts),
EventType: string(domain.UpdateEventEditMessage),
ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID),
ExcludeSessionID: dispatchSessionID,
}); err != nil {
return res, fmt.Errorf("enqueue edit message dispatch: %w", err)
}
res.Edited = append(res.Edited, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
}
if err := tx.Commit(ctx); err != nil {
return res, fmt.Errorf("commit edit message tx: %w", err)
}
committed = true
return res, nil
}
func validTodoParticipantEdit(req domain.EditMessageRequest, target sqlcgen.GetMessageBoxForEditRow, oldEntities []domain.MessageEntity) bool {
if !req.AllowTodoParticipantMutation || req.SetReplyMarkup || req.Media == nil || req.Media.Kind != domain.MessageMediaKindTodo || req.Media.Todo == nil {
return false
}
if target.MessageSenderID == req.OwnerUserID {
return false
}
if target.Body != req.Message || !sameMessageEntities(oldEntities, req.Entities) {
return false
}
targetMedia, err := decodeMessageMedia(target.MediaJson)
if err != nil || targetMedia == nil || targetMedia.Kind != domain.MessageMediaKindTodo || targetMedia.Todo == nil {
return false
}
return true
}