feat(messages): счётчики упоминаний, квота приглашений и read state автора

- упоминания в сообщении увеличивают счётчик упоминаний комнаты у упомянутых
  (store.BumpMentionCount) и отправляют им READ_STATE_UPDATE на все устройства;
- отправка сообщения отмечает его прочитанным для автора;
- подтверждение прочтения сбрасывает упоминания;
- суточная квота 10 приглашений на пользователя (AGENT.md 8.6), администратор
  инстанса её обходит; httpx.NewRateLimiterWindow для произвольных окон;
- тест на рост и сброс счётчика упоминаний.
This commit is contained in:
2026-09-19 23:37:02 +03:00
parent 0e39b8e027
commit 8619f37cd5
15 changed files with 186 additions and 6 deletions
+28
View File
@@ -159,6 +159,12 @@ func (s *Server) registerMessageRoutes(api huma.API) {
}
}
s.rememberSlowmode(channelID, user.ID)
// Своё сообщение считаем прочитанным, а упомянутым — увеличиваем
// счётчик упоминаний и сообщаем об этом их устройствам (AGENT.md 7.16).
if err := s.store.SetReadState(ctx, user.ID, channelID, message.ID, 0); err != nil {
s.logger.WarnContext(ctx, "failed to update author read state", slog.Any("error", err))
}
s.notifyMentions(ctx, channelID, user.ID, message.Mentions)
payload, err := s.messagePayload(ctx, message, user.ID)
if err != nil {
return nil, err
@@ -500,6 +506,28 @@ func (s *Server) registerMessageRoutes(api huma.API) {
})
}
// notifyMentions увеличивает счётчики упоминаний и уведомляет устройства
// упомянутых пользователей (AGENT.md 7.16).
func (s *Server) notifyMentions(ctx context.Context, channelID, authorID uint64, mentions []uint64) {
if s.gateway == nil || len(mentions) == 0 {
return
}
for _, userID := range mentions {
if userID == authorID {
continue
}
count, err := s.store.BumpMentionCount(ctx, userID, channelID)
if err != nil {
s.logger.WarnContext(ctx, "failed to bump mention count", slog.Any("error", err))
continue
}
s.gateway.SendToUser(userID, "READ_STATE_UPDATE", map[string]any{
"channel_id": formatSnowflake(channelID),
"mention_count": count,
})
}
}
// requireChannelPermission проверяет права пользователя в комнате и отдаёт её.
func (s *Server) requireChannelPermission(ctx context.Context, rawChannelID string, user *store.User, permission permissions.Permission) (uint64, permissions.Resolved, *store.Channel, error) {
channelID, err := parseID("channel_id", rawChannelID)