chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
394
internal/store/postgres/channel_updates.go
Normal file
394
internal/store/postgres/channel_updates.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
func (s *ChannelStore) ListChannelDifference(ctx context.Context, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error) {
|
||||
channel, member, preview, err := s.getChannelForViewer(ctx, s.db, req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
if req.Pts < 0 || req.Pts > channel.Pts {
|
||||
return domain.ChannelDifference{}, domain.ErrPersistentTimestamp
|
||||
}
|
||||
if !preview && member.AvailableMinPts > req.Pts {
|
||||
req.Pts = minInt(member.AvailableMinPts, channel.Pts)
|
||||
}
|
||||
limit := req.Limit
|
||||
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
|
||||
limit = domain.MaxChannelDifferenceLimit
|
||||
}
|
||||
if channel.Pts-req.Pts > limit {
|
||||
args := []any{req.ChannelID}
|
||||
where := "channel_id = $1 AND NOT deleted"
|
||||
if member.AvailableMinID > 0 {
|
||||
args = append(args, member.AvailableMinID)
|
||||
where += fmt.Sprintf(" AND id > $%d", len(args))
|
||||
}
|
||||
args = append(args, domain.MaxChannelDifferenceTooLongMessages)
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT `+channelMessageColumns+`
|
||||
FROM channel_messages
|
||||
WHERE `+where+`
|
||||
ORDER BY id DESC
|
||||
LIMIT $`+fmt.Sprint(len(args)), args...)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, fmt.Errorf("list channel too long messages: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
diff := domain.ChannelDifference{
|
||||
Channel: channel,
|
||||
Self: member,
|
||||
Pts: channel.Pts,
|
||||
Final: true,
|
||||
TooLong: true,
|
||||
Timeout: 30,
|
||||
}
|
||||
for rows.Next() {
|
||||
msg, err := scanChannelMessage(rows)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
diff.NewMessages = append(diff.NewMessages, msg)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, diff.NewMessages); err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
if preview {
|
||||
diff.Dialog = previewChannelDialog(req.UserID, channel, member)
|
||||
} else {
|
||||
dialog, err := s.getChannelDialog(ctx, s.db, req.UserID, channel)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
diff.Dialog = dialog
|
||||
}
|
||||
return diff, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT channel_id, pts, pts_count, date, event_type, message_id, message_ids::text, sender_user_id, user_ids::text, payload::text
|
||||
FROM channel_update_events
|
||||
WHERE channel_id = $1 AND pts > $2
|
||||
ORDER BY pts ASC
|
||||
LIMIT $3`, req.ChannelID, req.Pts, limit)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, fmt.Errorf("list channel difference: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
diff := domain.ChannelDifference{Channel: channel, Self: member, Pts: channel.Pts, Final: true, Timeout: 30}
|
||||
userRefs := make(map[int64]struct{})
|
||||
channelRefs := make(map[int64]struct{})
|
||||
lastPts := req.Pts
|
||||
for rows.Next() {
|
||||
event, messageID, err := scanChannelEvent(rows)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
ptsCount := event.PtsCount
|
||||
if ptsCount <= 0 {
|
||||
ptsCount = 1
|
||||
}
|
||||
if event.Pts != lastPts+ptsCount {
|
||||
s.log.Warn("channel_difference_stopped_at_gap",
|
||||
zap.String("scope", "channel"),
|
||||
zap.Int64("user_id", req.UserID),
|
||||
zap.Int64("channel_id", req.ChannelID),
|
||||
zap.Int("request_pts", req.Pts),
|
||||
zap.Int("channel_pts", channel.Pts),
|
||||
zap.Int("returned_pts", lastPts),
|
||||
zap.Int("expected_pts", lastPts+ptsCount),
|
||||
zap.Int("got_pts", event.Pts),
|
||||
zap.Int("got_pts_count", ptsCount),
|
||||
zap.String("event_type", string(event.Type)),
|
||||
zap.Int("limit", limit),
|
||||
)
|
||||
break
|
||||
}
|
||||
lastPts = event.Pts
|
||||
if messageID != 0 && event.Message.ID == 0 {
|
||||
msg, err := s.getChannelMessage(ctx, s.db, req.ChannelID, messageID)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
event.Message = msg
|
||||
}
|
||||
visibleEvent, ok := domain.FilterChannelUpdateEventForAvailableMinID(event, member.AvailableMinID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
event = visibleEvent
|
||||
if preview && event.Type == domain.ChannelUpdateParticipant {
|
||||
continue
|
||||
}
|
||||
collectChannelEventRefs(event, req.ChannelID, userRefs, channelRefs)
|
||||
diff.Events = append(diff.Events, event)
|
||||
diff.Pts = event.Pts
|
||||
switch event.Type {
|
||||
case domain.ChannelUpdateNewMessage:
|
||||
diff.NewMessages = append(diff.NewMessages, event.Message)
|
||||
default:
|
||||
diff.OtherUpdates = append(diff.OtherUpdates, event)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
if len(diff.Events) == 0 {
|
||||
diff.Pts = lastPts
|
||||
} else if lastPts > diff.Pts {
|
||||
diff.Pts = lastPts
|
||||
}
|
||||
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, diff.NewMessages); err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
// OtherUpdates 里带消息的事件未读/提及标记一次批量回填(原来逐事件一条 SQL 的 N+1)。
|
||||
otherMsgs := make([]domain.ChannelMessage, 0, len(diff.OtherUpdates))
|
||||
otherIdx := make([]int, 0, len(diff.OtherUpdates))
|
||||
for i := range diff.OtherUpdates {
|
||||
if diff.OtherUpdates[i].Message.ID == 0 {
|
||||
continue
|
||||
}
|
||||
otherMsgs = append(otherMsgs, diff.OtherUpdates[i].Message)
|
||||
otherIdx = append(otherIdx, i)
|
||||
}
|
||||
if len(otherMsgs) > 0 {
|
||||
if err := populateChannelMessageUnreadFlags(ctx, s.db, req.UserID, otherMsgs); err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
for j, i := range otherIdx {
|
||||
diff.OtherUpdates[i].Message = otherMsgs[j]
|
||||
}
|
||||
}
|
||||
users, err := listUsersByIDs(ctx, s.db, mapKeysInt64(userRefs))
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
channels, err := listChannelsByIDs(ctx, s.db, mapKeysInt64(channelRefs))
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
diff.Users = users
|
||||
diff.Channels = channels
|
||||
if preview {
|
||||
diff.Dialog = previewChannelDialog(req.UserID, channel, member)
|
||||
} else {
|
||||
dialog, err := s.getChannelDialog(ctx, s.db, req.UserID, channel)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
}
|
||||
diff.Dialog = dialog
|
||||
}
|
||||
diff.Final = lastPts >= channel.Pts
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) MaxChannelPts(ctx context.Context, channelID int64) (int, error) {
|
||||
var pts int
|
||||
err := s.db.QueryRow(ctx, `SELECT pts FROM channels WHERE id = $1`, channelID).Scan(&pts)
|
||||
return pts, err
|
||||
}
|
||||
|
||||
func transientChannelParticipantEvent(channelID, actorUserID int64, previous, participant domain.ChannelMember, date int) domain.ChannelUpdateEvent {
|
||||
return domain.ChannelUpdateEvent{
|
||||
ChannelID: channelID,
|
||||
Type: domain.ChannelUpdateParticipant,
|
||||
Date: date,
|
||||
SenderUserID: actorUserID,
|
||||
UserIDs: uniqueNonZeroInt64s(actorUserID, previous.UserID, previous.InviterUserID, participant.UserID, participant.InviterUserID),
|
||||
Previous: previous,
|
||||
Participant: participant,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ChannelStore) reserveChannelPts(ctx context.Context, db sqlcgen.DBTX, channelID int64) (int, error) {
|
||||
return s.reserveChannelPtsN(ctx, db, channelID, 1)
|
||||
}
|
||||
|
||||
func (s *ChannelStore) reserveChannelPtsN(ctx context.Context, db sqlcgen.DBTX, channelID int64, count int) (int, error) {
|
||||
count = normalizePtsCount(count)
|
||||
caller := traceCaller(2)
|
||||
pts, err := reserveChannelPts(ctx, db, channelID, count)
|
||||
if err != nil {
|
||||
s.log.Warn("pts_reserve_failed",
|
||||
zap.String("scope", "channel"),
|
||||
zap.Int64("channel_id", channelID),
|
||||
zap.Int("pts_count", count),
|
||||
zap.String("caller", traceCaller(2)),
|
||||
zap.Error(err),
|
||||
zap.Error(ctx.Err()),
|
||||
)
|
||||
return 0, err
|
||||
}
|
||||
s.log.Debug("pts_reserve",
|
||||
zap.String("scope", "channel"),
|
||||
zap.Int64("channel_id", channelID),
|
||||
zap.Int("pts", pts),
|
||||
zap.Int("pts_count", maxInt(count, 1)),
|
||||
zap.String("caller", caller),
|
||||
)
|
||||
return pts, nil
|
||||
}
|
||||
|
||||
func reserveChannelPts(ctx context.Context, db sqlcgen.DBTX, channelID int64, count int) (int, error) {
|
||||
count = normalizePtsCount(count)
|
||||
if channelID == 0 {
|
||||
return 0, fmt.Errorf("channel pts: missing channel id")
|
||||
}
|
||||
var pts int
|
||||
if err := db.QueryRow(ctx, `
|
||||
UPDATE channels
|
||||
SET pts = pts + $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING pts`, channelID, count).Scan(&pts); err != nil {
|
||||
return 0, fmt.Errorf("reserve channel pts: %w", err)
|
||||
}
|
||||
return pts, nil
|
||||
}
|
||||
|
||||
func insertChannelEventTx(ctx context.Context, tx pgx.Tx, event domain.ChannelUpdateEvent) error {
|
||||
ids, err := marshalJSON(event.MessageIDs, "[]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDs, err := marshalJSON(event.UserIDs, "[]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payloadData := map[string]any{
|
||||
"message_id": event.Message.ID,
|
||||
"pinned": event.Pinned,
|
||||
}
|
||||
if event.Message.ID != 0 {
|
||||
payloadData["message"] = event.Message
|
||||
}
|
||||
if event.Previous.UserID != 0 {
|
||||
payloadData["previous_participant"] = event.Previous
|
||||
}
|
||||
if event.Participant.UserID != 0 {
|
||||
payloadData["participant"] = event.Participant
|
||||
}
|
||||
payload, err := marshalJSON(payloadData, "{}")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO channel_update_events (
|
||||
channel_id, pts, pts_count, date, event_type, message_id, message_ids, sender_user_id, user_ids, payload
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
|
||||
event.ChannelID, event.Pts, event.PtsCount, event.Date, string(event.Type), event.Message.ID,
|
||||
ids, event.SenderUserID, userIDs, payload); err != nil {
|
||||
return fmt.Errorf("insert channel event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanChannelEvent(row rowScanner) (domain.ChannelUpdateEvent, int, error) {
|
||||
var event domain.ChannelUpdateEvent
|
||||
var typ string
|
||||
var messageID int
|
||||
var messageIDs, userIDs, payload string
|
||||
if err := row.Scan(
|
||||
&event.ChannelID, &event.Pts, &event.PtsCount, &event.Date, &typ, &messageID,
|
||||
&messageIDs, &event.SenderUserID, &userIDs, &payload,
|
||||
); err != nil {
|
||||
return domain.ChannelUpdateEvent{}, 0, err
|
||||
}
|
||||
event.Type = domain.ChannelUpdateEventType(typ)
|
||||
_ = json.Unmarshal([]byte(messageIDs), &event.MessageIDs)
|
||||
_ = json.Unmarshal([]byte(userIDs), &event.UserIDs)
|
||||
var data struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
Message domain.ChannelMessage `json:"message"`
|
||||
PreviousParticipant domain.ChannelMember `json:"previous_participant"`
|
||||
Participant domain.ChannelMember `json:"participant"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(payload), &data)
|
||||
event.Pinned = data.Pinned
|
||||
if data.Message.ID != 0 {
|
||||
event.Message = data.Message
|
||||
}
|
||||
event.Previous = data.PreviousParticipant
|
||||
event.Participant = data.Participant
|
||||
return event, messageID, nil
|
||||
}
|
||||
|
||||
func channelInitialAvailableMinPts(channel domain.Channel) int {
|
||||
return channel.Pts
|
||||
}
|
||||
|
||||
func adminLogEventTypesForFilter(filter domain.ChannelAdminLogFilter) []string {
|
||||
if filter.Empty() {
|
||||
return nil
|
||||
}
|
||||
types := make([]string, 0, 16)
|
||||
add := func(enabled bool, typ domain.ChannelAdminLogEventType) {
|
||||
if enabled {
|
||||
types = append(types, string(typ))
|
||||
}
|
||||
}
|
||||
add(filter.Join, domain.ChannelAdminLogParticipantJoin)
|
||||
add(filter.Leave, domain.ChannelAdminLogParticipantLeave)
|
||||
add(filter.Invite || filter.Invites, domain.ChannelAdminLogParticipantInvite)
|
||||
add(filter.Ban, domain.ChannelAdminLogParticipantBan)
|
||||
add(filter.Unban, domain.ChannelAdminLogParticipantUnban)
|
||||
add(filter.Kick, domain.ChannelAdminLogParticipantKick)
|
||||
add(filter.Unkick, domain.ChannelAdminLogParticipantUnkick)
|
||||
add(filter.Promote, domain.ChannelAdminLogParticipantPromote)
|
||||
add(filter.Demote, domain.ChannelAdminLogParticipantDemote)
|
||||
add(filter.EditRank, domain.ChannelAdminLogParticipantEditRank)
|
||||
if filter.Info {
|
||||
types = append(types,
|
||||
string(domain.ChannelAdminLogChangeTitle),
|
||||
string(domain.ChannelAdminLogChangeUsername),
|
||||
string(domain.ChannelAdminLogChangeLinkedChat),
|
||||
string(domain.ChannelAdminLogToggleSlowMode),
|
||||
)
|
||||
}
|
||||
if filter.Settings {
|
||||
types = append(types,
|
||||
string(domain.ChannelAdminLogToggleSignatures),
|
||||
string(domain.ChannelAdminLogTogglePreHistoryHidden),
|
||||
string(domain.ChannelAdminLogToggleAntiSpam),
|
||||
string(domain.ChannelAdminLogToggleAutotranslation),
|
||||
)
|
||||
}
|
||||
add(filter.Forums || filter.Settings, domain.ChannelAdminLogToggleForum)
|
||||
add(filter.Pinned, domain.ChannelAdminLogUpdatePinned)
|
||||
add(filter.Edit, domain.ChannelAdminLogEditMessage)
|
||||
add(filter.Delete, domain.ChannelAdminLogDeleteMessage)
|
||||
add(filter.Send, domain.ChannelAdminLogSendMessage)
|
||||
return types
|
||||
}
|
||||
|
||||
func collectChannelEventRefs(event domain.ChannelUpdateEvent, currentChannelID int64, userRefs, channelRefs map[int64]struct{}) {
|
||||
if event.SenderUserID != 0 {
|
||||
userRefs[event.SenderUserID] = struct{}{}
|
||||
}
|
||||
for _, id := range event.UserIDs {
|
||||
if id != 0 {
|
||||
userRefs[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, member := range []domain.ChannelMember{event.Previous, event.Participant} {
|
||||
if member.UserID != 0 {
|
||||
userRefs[member.UserID] = struct{}{}
|
||||
}
|
||||
if member.InviterUserID != 0 {
|
||||
userRefs[member.InviterUserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
collectChannelMessageRefs(event.Message, currentChannelID, userRefs, channelRefs)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue