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,443 @@
package postgres
import (
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"hash"
"sort"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
"time"
)
func (s *MessageStore) SetMessageReactions(ctx context.Context, req domain.SetPrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
if req.UserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if len(req.Reactions) > domain.MaxChannelMessageReactionsPerUser {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
req.Reactions = domain.TrimMessageReactionsToUserMax(req.Reactions, req.ReactionsPerUserMax)
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
for _, reaction := range req.Reactions {
if !reaction.Valid() {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("set message reactions: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("begin set message reactions tx: %w", err)
}
qtx := sqlcgen.New(tx)
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
if err := lockUsersForUpdate(ctx, tx, req.UserID, req.Peer.ID); err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("lock set message reactions users: %w", err)
}
var target struct {
boxID int32
privateMessageID int64
messageSenderID int64
}
if err := tx.QueryRow(ctx, `
SELECT box_id, private_message_id, message_sender_id
FROM message_boxes
WHERE owner_user_id = $1
AND box_id = $2
AND peer_type = $3
AND peer_id = $4
AND NOT deleted
LIMIT 1
FOR UPDATE`, req.UserID, int32(req.MessageID), string(req.Peer.Type), req.Peer.ID).Scan(&target.boxID, &target.privateMessageID, &target.messageSenderID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("get message for reactions: %w", err)
}
if _, err := tx.Exec(ctx, `
DELETE FROM private_message_reactions
WHERE message_sender_id = $1
AND private_message_id = $2
AND user_id = $3`, target.messageSenderID, target.privateMessageID, req.UserID); err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("delete old message reactions: %w", err)
}
for i, reaction := range req.Reactions {
if _, err := tx.Exec(ctx, `
INSERT INTO private_message_reactions (
message_sender_id,
private_message_id,
user_id,
reaction_type,
reaction_value,
big,
reaction_date,
chosen_order
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (message_sender_id, private_message_id, user_id, reaction_type, reaction_value)
DO UPDATE SET
big = EXCLUDED.big,
reaction_date = EXCLUDED.reaction_date,
chosen_order = EXCLUDED.chosen_order,
updated_at = now()`,
target.messageSenderID,
target.privateMessageID,
req.UserID,
string(reaction.Type),
reaction.Value(),
req.Big,
int32(req.Date),
int32(i+1),
); err != nil {
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{
OwnerUserIds: privateMessageOwnerIDs(req.UserID, req.Peer.ID),
MessageSenderID: target.messageSenderID,
PrivateMessageID: target.privateMessageID,
})
if err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("list visible reaction boxes: %w", err)
}
res := domain.PrivateMessageReactionsResult{Messages: make([]domain.Message, 0, len(boxes))}
for _, box := range boxes {
msg, err := messageFromVisibleBoxRow(box)
if err != nil {
return domain.PrivateMessageReactionsResult{}, err
}
res.Messages = append(res.Messages, msg)
}
if err := s.enrichPrivateMessageReactions(ctx, tx, req.UserID, res.Messages); err != nil {
return domain.PrivateMessageReactionsResult{}, err
}
for _, msg := range res.Messages {
if msg.OwnerUserID == req.UserID && msg.Reactions != nil {
res.Reactions = *msg.Reactions
break
}
}
if err := tx.Commit(ctx); err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("commit set message reactions tx: %w", err)
}
committed = true
return res, nil
}
func (s *MessageStore) GetMessageReactions(ctx context.Context, req domain.PrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
if req.OwnerUserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || len(req.IDs) > domain.MaxGetMessageIDs {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if len(req.IDs) == 0 {
return domain.PrivateMessageReactionsResult{}, nil
}
boxIDs := make([]int32, 0, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
boxIDs = append(boxIDs, int32(id))
}
rows, err := s.q.GetMessageBoxesByIDs(ctx, sqlcgen.GetMessageBoxesByIDsParams{
OwnerUserID: req.OwnerUserID,
BoxIds: boxIDs,
})
if err != nil {
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("get message reactions boxes: %w", err)
}
res := domain.PrivateMessageReactionsResult{Messages: make([]domain.Message, 0, len(rows))}
for _, row := range rows {
if row.PeerType != string(req.Peer.Type) || row.PeerID != req.Peer.ID {
continue
}
msg, err := messageFromIDRow(row)
if err != nil {
return domain.PrivateMessageReactionsResult{}, err
}
res.Messages = append(res.Messages, msg)
}
if err := s.enrichPrivateMessageReactions(ctx, s.db, req.OwnerUserID, res.Messages); err != nil {
return domain.PrivateMessageReactionsResult{}, err
}
for _, msg := range res.Messages {
if msg.Reactions != nil {
res.Reactions = *msg.Reactions
break
}
}
return res, nil
}
type privateMessageReactionRow struct {
messageSenderID int64
privateMessageID int64
userID int64
reaction domain.MessageReaction
big bool
date int
chosenOrder int
}
type privateMessageReactionKey struct {
messageSenderID int64
privateMessageID int64
}
func (s *MessageStore) enrichPrivateMessageReactions(ctx context.Context, db sqlcgen.DBTX, viewerUserID int64, messages []domain.Message) error {
if len(messages) == 0 {
return nil
}
// poll enrichment 与 reactions 同点位挂载:所有私聊消息读路径都经过本函数,
// poll media 的权威 closed/聚合/viewer 门控在此一并填充(见 message_polls.go)。
if err := s.enrichPrivateMessagePolls(ctx, db, viewerUserID, messages); err != nil {
return err
}
keySet := make(map[privateMessageReactionKey]struct{}, len(messages))
senderIDs := make([]int64, 0, len(messages))
privateIDs := make([]int64, 0, len(messages))
for _, msg := range messages {
if msg.UID == 0 || msg.From.ID == 0 {
continue
}
key := privateMessageReactionKey{messageSenderID: msg.From.ID, privateMessageID: msg.UID}
if _, ok := keySet[key]; ok {
continue
}
keySet[key] = struct{}{}
senderIDs = append(senderIDs, key.messageSenderID)
privateIDs = append(privateIDs, key.privateMessageID)
}
if len(senderIDs) == 0 {
return nil
}
rows, err := db.Query(ctx, `
WITH wanted AS (
SELECT message_sender_id, private_message_id
FROM unnest($1::bigint[], $2::bigint[]) AS w(message_sender_id, private_message_id)
)
SELECT r.message_sender_id, r.private_message_id, r.user_id, r.reaction_type, r.reaction_value, r.big, r.reaction_date, r.chosen_order
FROM private_message_reactions r
JOIN wanted w
ON w.message_sender_id = r.message_sender_id
AND w.private_message_id = r.private_message_id
ORDER BY r.message_sender_id ASC, r.private_message_id ASC, r.reaction_date DESC, r.user_id DESC, r.reaction_type ASC, r.reaction_value ASC`, senderIDs, privateIDs)
if err != nil {
return fmt.Errorf("load private message reactions: %w", err)
}
defer rows.Close()
byMessage := make(map[privateMessageReactionKey][]privateMessageReactionRow)
for rows.Next() {
var (
messageSenderID int64
uid int64
userID int64
reactionType string
value string
big bool
date int32
chosenOrder int32
)
if err := rows.Scan(&messageSenderID, &uid, &userID, &reactionType, &value, &big, &date, &chosenOrder); err != nil {
return fmt.Errorf("scan private message reactions: %w", err)
}
reaction, ok := domain.MessageReactionFromValue(domain.MessageReactionType(reactionType), value)
if !ok {
continue
}
key := privateMessageReactionKey{messageSenderID: messageSenderID, privateMessageID: uid}
byMessage[key] = append(byMessage[key], privateMessageReactionRow{
messageSenderID: messageSenderID,
privateMessageID: uid,
userID: userID,
reaction: reaction,
big: big,
date: int(date),
chosenOrder: int(chosenOrder),
})
}
if err := rows.Err(); err != nil {
return fmt.Errorf("private message reactions rows: %w", err)
}
for i := range messages {
key := privateMessageReactionKey{messageSenderID: messages[i].From.ID, privateMessageID: messages[i].UID}
// chosen/My 是 per-viewer 字段,必须按该副本的 box owner 视角解析:
// SetMessageReactions 会同时返回双方 owner 的副本,若统一用请求者视角,
// 对端收到的 updateMessageReactions 会把发起者的 reaction 标成"自己选的"
//(TDesktop 非 min 更新直接以 chosen_order 覆盖本地 my 状态)。
viewpoint := messages[i].OwnerUserID
if viewpoint == 0 {
viewpoint = viewerUserID
}
reactions := privateMessageReactionsFromRows(byMessage[key], viewpoint)
applyPrivateMessageReactionUnread(&reactions, messages[i])
if len(reactions.Results) == 0 && len(reactions.Recent) == 0 {
continue
}
messages[i].Reactions = &reactions
}
return nil
}
func applyPrivateMessageReactionUnread(reactions *domain.ChannelMessageReactions, msg domain.Message) {
if reactions == nil || len(reactions.Recent) == 0 || msg.From.ID == 0 {
return
}
for i := range reactions.Recent {
reactions.Recent[i].SenderUserID = msg.From.ID
if msg.ReactionUnread && msg.From.ID == msg.OwnerUserID && reactions.Recent[i].UserID != msg.OwnerUserID {
reactions.Recent[i].Unread = true
}
}
}
func privateMessageReactionsFromRows(rows []privateMessageReactionRow, viewerUserID int64) domain.ChannelMessageReactions {
out := domain.ChannelMessageReactions{
CanSeeList: true,
Results: []domain.ChannelMessageReactionCount{},
Recent: []domain.ChannelMessagePeerReaction{},
}
if len(rows) == 0 {
return out
}
type aggregate struct {
reaction domain.MessageReaction
count int
chosenOrder int
latestDate int
}
aggregates := make(map[string]*aggregate)
recent := make([]domain.ChannelMessagePeerReaction, 0, len(rows))
for _, row := range rows {
key := row.reaction.Key()
item := aggregates[key]
if item == nil {
item = &aggregate{reaction: row.reaction}
aggregates[key] = item
}
item.count++
if row.userID == viewerUserID && row.chosenOrder > 0 && (item.chosenOrder == 0 || row.chosenOrder < item.chosenOrder) {
item.chosenOrder = row.chosenOrder
}
if row.date > item.latestDate {
item.latestDate = row.date
}
recent = append(recent, domain.ChannelMessagePeerReaction{
UserID: row.userID,
Reaction: row.reaction,
Big: row.big,
My: row.userID == viewerUserID,
ChosenOrder: row.chosenOrder,
Date: row.date,
})
}
items := make([]aggregate, 0, len(aggregates))
for _, item := range aggregates {
items = append(items, *item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].count != items[j].count {
return items[i].count > items[j].count
}
if items[i].latestDate != items[j].latestDate {
return items[i].latestDate > items[j].latestDate
}
return items[i].reaction.Key() < items[j].reaction.Key()
})
for _, item := range items {
out.Results = append(out.Results, domain.ChannelMessageReactionCount{
Reaction: item.reaction,
Count: item.count,
ChosenOrder: item.chosenOrder,
})
}
sort.Slice(recent, func(i, j int) bool {
if recent[i].Date != recent[j].Date {
return recent[i].Date > recent[j].Date
}
if recent[i].UserID != recent[j].UserID {
return recent[i].UserID > recent[j].UserID
}
return recent[i].Reaction.Key() < recent[j].Reaction.Key()
})
if len(recent) > domain.MaxChannelMessageReactionRecent {
recent = recent[:domain.MaxChannelMessageReactionRecent]
}
out.Recent = recent
return out
}
func writeMessageReactionsHash(h hash.Hash64, reactions *domain.ChannelMessageReactions) {
if reactions == nil {
_, _ = h.Write([]byte{0})
return
}
var buf [16]byte
for _, item := range reactions.Results {
_, _ = h.Write([]byte(item.Reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(item.Reaction.Value()))
_, _ = h.Write([]byte{0})
binary.LittleEndian.PutUint32(buf[:4], uint32(item.Count))
binary.LittleEndian.PutUint32(buf[4:8], uint32(item.ChosenOrder))
_, _ = h.Write(buf[:8])
}
_, _ = h.Write([]byte{0xfe})
for _, item := range reactions.Recent {
_, _ = h.Write([]byte(item.Reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(item.Reaction.Value()))
_, _ = h.Write([]byte{0})
binary.LittleEndian.PutUint64(buf[:8], uint64(item.UserID))
binary.LittleEndian.PutUint32(buf[8:12], uint32(item.Date))
binary.LittleEndian.PutUint32(buf[12:16], uint32(item.ChosenOrder))
_, _ = h.Write(buf[:])
}
}