chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
311
internal/store/postgres/message_pin.go
Normal file
311
internal/store/postgres/message_pin.go
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// PinPrivateMessage 翻转私聊消息的置顶状态。官方语义:
|
||||
// - pin 且非 pm_oneside:双方 box 行同步置位(对端 box 经
|
||||
// private_message_id 翻译),双方各收一条带账号 pts 的
|
||||
// updatePinnedMessages;
|
||||
// - pin 且 pm_oneside / Saved Messages:仅本侧;
|
||||
// - unpin:无 oneside 形态,双侧清除(对端行未置顶时自然跳过);
|
||||
// - 状态已是目标值:幂等 no-op,不烧 pts、不发事件。
|
||||
//
|
||||
// messageActionPinMessage 服务消息不在此生成(走 SendPrivateText 服务
|
||||
// 消息通道,置顶状态本身是真值源,服务消息仅为时间线装饰)。
|
||||
func (s *MessageStore) PinPrivateMessage(ctx context.Context, req domain.PinPrivateMessageRequest) (res domain.PinPrivateMessageResult, err error) {
|
||||
res = domain.PinPrivateMessageResult{OwnerUserID: req.OwnerUserID}
|
||||
if req.OwnerUserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if req.Date == 0 {
|
||||
req.Date = int(time.Now().Unix())
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("pin private message: db does not support transactions")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("begin pin message tx: %w", err)
|
||||
}
|
||||
qtx := sqlcgen.New(tx)
|
||||
committed := false
|
||||
defer func() {
|
||||
if committed {
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
if err := lockUsersForUpdate(ctx, tx, req.OwnerUserID, req.Peer.ID); err != nil {
|
||||
return res, fmt.Errorf("lock pin users: %w", err)
|
||||
}
|
||||
owned, err := qtx.GetMessageBoxForPin(ctx, sqlcgen.GetMessageBoxForPinParams{
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
PeerType: string(req.Peer.Type),
|
||||
PeerID: req.Peer.ID,
|
||||
BoxID: int32(req.MessageID),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
return res, fmt.Errorf("get message for pin: %w", err)
|
||||
}
|
||||
// 服务消息不可置顶(官方 canPin 排除 isService)。
|
||||
if media, mediaErr := decodeMessageMedia(owned.MediaJson); mediaErr == nil &&
|
||||
media != nil && media.Kind == domain.MessageMediaKindService {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if owned.Pinned == req.Pinned && (!req.Pinned || req.PmOneside) {
|
||||
// 幂等:本侧已是目标状态。unpin 与 oneside pin 到此即 no-op;
|
||||
// 共享 pin 不在此短路——本侧已置顶(如此前 oneside)时仍需向
|
||||
// 对端传播补置顶与服务消息。
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return res, fmt.Errorf("commit pin message tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type pinSide struct {
|
||||
userID int64
|
||||
peer domain.Peer
|
||||
boxID int
|
||||
}
|
||||
sides := []pinSide{{userID: req.OwnerUserID, peer: req.Peer, boxID: int(owned.BoxID)}}
|
||||
// 对端侧翻转:pin 仅在非 oneside 时传播;unpin 恒尝试清除(oneside
|
||||
// pin 的对端行本就未置顶,SetMessageBoxPinned 0 行自然跳过)。
|
||||
propagatePeer := req.Peer.ID != req.OwnerUserID && (!req.Pinned || !req.PmOneside)
|
||||
if propagatePeer {
|
||||
peerRow, peerErr := qtx.GetMessageBoxByPrivateMessage(ctx, sqlcgen.GetMessageBoxByPrivateMessageParams{
|
||||
OwnerUserID: req.Peer.ID,
|
||||
PrivateMessageID: owned.PrivateMessageID,
|
||||
})
|
||||
if peerErr == nil {
|
||||
sides = append(sides, pinSide{
|
||||
userID: req.Peer.ID,
|
||||
peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID},
|
||||
boxID: int(peerRow.BoxID),
|
||||
})
|
||||
} else if !errors.Is(peerErr, pgx.ErrNoRows) {
|
||||
return res, fmt.Errorf("get peer message for pin: %w", peerErr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, side := range sides {
|
||||
affected, err := qtx.SetMessageBoxPinned(ctx, sqlcgen.SetMessageBoxPinnedParams{
|
||||
Pinned: req.Pinned,
|
||||
OwnerUserID: side.userID,
|
||||
BoxID: int32(side.boxID),
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("set message pinned: %w", err)
|
||||
}
|
||||
if affected == 0 {
|
||||
continue
|
||||
}
|
||||
pts, err := s.reservePts(ctx, tx, side.userID)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("allocate pin pts: %w", err)
|
||||
}
|
||||
event := domain.UpdateEvent{
|
||||
UserID: side.userID,
|
||||
Type: domain.UpdateEventPinnedMessages,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: req.Date,
|
||||
Peer: side.peer,
|
||||
Bool: req.Pinned,
|
||||
MessageIDs: []int{side.boxID},
|
||||
}
|
||||
if err := appendUserUpdateEvent(ctx, tx, qtx, side.userID, event); err != nil {
|
||||
return res, fmt.Errorf("append pinned messages event: %w", err)
|
||||
}
|
||||
dispatchAuthKeyID := [8]byte{}
|
||||
dispatchSessionID := int64(0)
|
||||
if side.userID == req.OwnerUserID {
|
||||
dispatchAuthKeyID = req.OriginAuthKeyID
|
||||
dispatchSessionID = req.OriginSessionID
|
||||
}
|
||||
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
|
||||
TargetUserID: side.userID,
|
||||
Pts: int32(pts),
|
||||
EventType: string(domain.UpdateEventPinnedMessages),
|
||||
ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID),
|
||||
ExcludeSessionID: dispatchSessionID,
|
||||
}); err != nil {
|
||||
return res, fmt.Errorf("enqueue pinned messages dispatch: %w", err)
|
||||
}
|
||||
res.Updated = append(res.Updated, domain.PinnedMessagesForUser{
|
||||
UserID: side.userID,
|
||||
Peer: side.peer,
|
||||
MessageIDs: []int{side.boxID},
|
||||
Pinned: req.Pinned,
|
||||
Event: event,
|
||||
})
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return res, fmt.Errorf("commit pin message tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UnpinAllPrivateMessages 清空与某私聊 peer 的全部置顶。本侧整批清除,
|
||||
// 共享置顶经 private_message_id 同步清除对端行;双方各收一条带账号 pts
|
||||
// 的 updatePinnedMessages{pinned:false},messages 为各自视角 box id。
|
||||
func (s *MessageStore) UnpinAllPrivateMessages(ctx context.Context, req domain.UnpinAllPrivateMessagesRequest) (res domain.PinPrivateMessageResult, err error) {
|
||||
res = domain.PinPrivateMessageResult{OwnerUserID: req.OwnerUserID}
|
||||
if req.OwnerUserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if req.Date == 0 {
|
||||
req.Date = int(time.Now().Unix())
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("unpin all private messages: db does not support transactions")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("begin unpin all tx: %w", err)
|
||||
}
|
||||
qtx := sqlcgen.New(tx)
|
||||
committed := false
|
||||
defer func() {
|
||||
if committed {
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
if err := lockUsersForUpdate(ctx, tx, req.OwnerUserID, req.Peer.ID); err != nil {
|
||||
return res, fmt.Errorf("lock unpin users: %w", err)
|
||||
}
|
||||
ownRows, err := qtx.UnpinAllMessageBoxesByPeer(ctx, sqlcgen.UnpinAllMessageBoxesByPeerParams{
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
PeerType: string(req.Peer.Type),
|
||||
PeerID: req.Peer.ID,
|
||||
LimitCount: domain.MaxUnpinAllBatch,
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("unpin own messages: %w", err)
|
||||
}
|
||||
if len(ownRows) == 0 {
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return res, fmt.Errorf("commit unpin all tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return res, nil
|
||||
}
|
||||
if len(ownRows) == domain.MaxUnpinAllBatch {
|
||||
more, moreErr := qtx.HasPinnedMessageBoxByPeer(ctx, sqlcgen.HasPinnedMessageBoxByPeerParams{
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
PeerType: string(req.Peer.Type),
|
||||
PeerID: req.Peer.ID,
|
||||
})
|
||||
if moreErr != nil {
|
||||
return res, fmt.Errorf("check remaining pinned after unpin all: %w", moreErr)
|
||||
}
|
||||
if more {
|
||||
res.Offset = 1
|
||||
}
|
||||
}
|
||||
|
||||
type unpinSide struct {
|
||||
userID int64
|
||||
peer domain.Peer
|
||||
ids []int
|
||||
}
|
||||
ownIDs := make([]int, 0, len(ownRows))
|
||||
senderIDs := make([]int64, 0, len(ownRows))
|
||||
pmIDs := make([]int64, 0, len(ownRows))
|
||||
for _, row := range ownRows {
|
||||
ownIDs = append(ownIDs, int(row.BoxID))
|
||||
senderIDs = append(senderIDs, row.MessageSenderID)
|
||||
pmIDs = append(pmIDs, row.PrivateMessageID)
|
||||
}
|
||||
sides := []unpinSide{{userID: req.OwnerUserID, peer: req.Peer, ids: ownIDs}}
|
||||
if req.Peer.ID != req.OwnerUserID {
|
||||
peerBoxIDs, err := qtx.UnpinMessageBoxesByPrivateMessages(ctx, sqlcgen.UnpinMessageBoxesByPrivateMessagesParams{
|
||||
MessageSenderIds: senderIDs,
|
||||
PrivateMessageIds: pmIDs,
|
||||
OwnerUserID: req.Peer.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("unpin peer messages: %w", err)
|
||||
}
|
||||
if len(peerBoxIDs) > 0 {
|
||||
peerIDs := make([]int, 0, len(peerBoxIDs))
|
||||
for _, id := range peerBoxIDs {
|
||||
peerIDs = append(peerIDs, int(id))
|
||||
}
|
||||
sides = append(sides, unpinSide{
|
||||
userID: req.Peer.ID,
|
||||
peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID},
|
||||
ids: peerIDs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, side := range sides {
|
||||
pts, err := s.reservePts(ctx, tx, side.userID)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("allocate unpin all pts: %w", err)
|
||||
}
|
||||
event := domain.UpdateEvent{
|
||||
UserID: side.userID,
|
||||
Type: domain.UpdateEventPinnedMessages,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: req.Date,
|
||||
Peer: side.peer,
|
||||
Bool: false,
|
||||
MessageIDs: side.ids,
|
||||
}
|
||||
if err := appendUserUpdateEvent(ctx, tx, qtx, side.userID, event); err != nil {
|
||||
return res, fmt.Errorf("append unpin all event: %w", err)
|
||||
}
|
||||
dispatchAuthKeyID := [8]byte{}
|
||||
dispatchSessionID := int64(0)
|
||||
if side.userID == req.OwnerUserID {
|
||||
dispatchAuthKeyID = req.OriginAuthKeyID
|
||||
dispatchSessionID = req.OriginSessionID
|
||||
}
|
||||
if err := qtx.EnqueueDispatch(ctx, sqlcgen.EnqueueDispatchParams{
|
||||
TargetUserID: side.userID,
|
||||
Pts: int32(pts),
|
||||
EventType: string(domain.UpdateEventPinnedMessages),
|
||||
ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID),
|
||||
ExcludeSessionID: dispatchSessionID,
|
||||
}); err != nil {
|
||||
return res, fmt.Errorf("enqueue unpin all dispatch: %w", err)
|
||||
}
|
||||
res.Updated = append(res.Updated, domain.PinnedMessagesForUser{
|
||||
UserID: side.userID,
|
||||
Peer: side.peer,
|
||||
MessageIDs: side.ids,
|
||||
Pinned: false,
|
||||
Event: event,
|
||||
})
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return res, fmt.Errorf("commit unpin all tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return res, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue