chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
766
cmd/telesrv-admin/readstore.go
Normal file
766
cmd/telesrv-admin/readstore.go
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const (
|
||||
accountSearchLimit = 20
|
||||
accountListDefaultLimit = 50
|
||||
accountListMaxLimit = 100
|
||||
channelSearchLimit = 50
|
||||
channelListDefaultLimit = 50
|
||||
channelListMaxLimit = 100
|
||||
messagePageLimit = 100
|
||||
)
|
||||
|
||||
type readStore struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func newReadStore(pool *pgxpool.Pool) *readStore {
|
||||
return &readStore{pool: pool}
|
||||
}
|
||||
|
||||
type AccountRow struct {
|
||||
ID int64
|
||||
Phone string
|
||||
Username string
|
||||
FirstName string
|
||||
LastName string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Frozen bool
|
||||
Reason string
|
||||
Verified bool
|
||||
PremiumUntil int64
|
||||
LastActiveAt time.Time
|
||||
DeviceCount int
|
||||
}
|
||||
|
||||
type AccountDetail struct {
|
||||
Account AccountRow
|
||||
About string
|
||||
LastSeenAt int64
|
||||
Verified bool
|
||||
Support bool
|
||||
Bot bool
|
||||
Restriction RestrictionRow
|
||||
HasRestriction bool
|
||||
Authorizations []AuthorizationRow
|
||||
AuditLogs []AuditLogRow
|
||||
}
|
||||
|
||||
type RestrictionRow struct {
|
||||
Frozen bool
|
||||
Reason string
|
||||
Actor string
|
||||
CommandID string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AuthorizationRow struct {
|
||||
AuthKeyID int64
|
||||
Hash int64
|
||||
Layer int
|
||||
DeviceModel string
|
||||
Platform string
|
||||
SystemVersion string
|
||||
APIID int
|
||||
AppVersion string
|
||||
IP string
|
||||
PasswordPending bool
|
||||
CreatedAt time.Time
|
||||
ActiveAt time.Time
|
||||
}
|
||||
|
||||
type AuditLogRow struct {
|
||||
ID int64
|
||||
CommandID string
|
||||
Actor string
|
||||
Action string
|
||||
DryRun bool
|
||||
Reason string
|
||||
Status string
|
||||
Error string
|
||||
Result string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ChannelRow struct {
|
||||
ID int64
|
||||
AccessHash int64
|
||||
CreatorUserID int64
|
||||
Title string
|
||||
About string
|
||||
Username string
|
||||
Broadcast bool
|
||||
Megagroup bool
|
||||
Forum bool
|
||||
Monoforum bool
|
||||
Verified bool
|
||||
Deleted bool
|
||||
ParticipantsCount int
|
||||
AdminsCount int
|
||||
KickedCount int
|
||||
BannedCount int
|
||||
TopMessageID int
|
||||
PinnedMessageID int
|
||||
PTS int
|
||||
Date int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ChannelDetail struct {
|
||||
Channel ChannelRow
|
||||
ChannelJSON string
|
||||
AuditLogs []AuditLogRow
|
||||
}
|
||||
|
||||
func (s *readStore) SearchAccounts(ctx context.Context, q string) ([]AccountRow, error) {
|
||||
q = strings.TrimSpace(q)
|
||||
if q == "" {
|
||||
return nil, nil
|
||||
}
|
||||
id := int64(-1)
|
||||
if n, err := strconv.ParseInt(q, 10, 64); err == nil {
|
||||
id = n
|
||||
}
|
||||
phone := strings.TrimPrefix(strings.ReplaceAll(q, " ", ""), "+")
|
||||
phoneRaw := strings.TrimSpace(q)
|
||||
username := strings.ToLower(strings.TrimPrefix(q, "@"))
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
WITH auth AS (
|
||||
SELECT user_id, max(active_at) AS last_active_at, count(*)::int AS device_count
|
||||
FROM authorizations
|
||||
GROUP BY user_id
|
||||
)
|
||||
SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at,
|
||||
COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified,
|
||||
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint,
|
||||
COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), COALESCE(a.device_count, 0)::int,
|
||||
COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username
|
||||
FROM users u
|
||||
LEFT JOIN account_send_restrictions r ON r.user_id = u.id
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id
|
||||
LEFT JOIN auth a ON a.user_id = u.id
|
||||
WHERE u.id = $1 OR u.phone = $2 OR u.phone = $3 OR lower(u.username) = $4 OR p.username_lower = $4
|
||||
ORDER BY u.id
|
||||
LIMIT $5`, id, phone, phoneRaw, username, accountSearchLimit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search accounts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AccountRow, 0)
|
||||
for rows.Next() {
|
||||
var item AccountRow
|
||||
if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) SearchChannels(ctx context.Context, q string) ([]ChannelRow, error) {
|
||||
q = strings.TrimSpace(q)
|
||||
if q == "" {
|
||||
return nil, nil
|
||||
}
|
||||
id := int64(-1)
|
||||
if n, err := strconv.ParseInt(q, 10, 64); err == nil {
|
||||
id = n
|
||||
}
|
||||
username := strings.ToLower(strings.TrimPrefix(q, "@"))
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about,
|
||||
COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username,
|
||||
c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted,
|
||||
c.participants_count, c.admins_count, c.kicked_count, c.banned_count,
|
||||
c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at
|
||||
FROM channels c
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id
|
||||
WHERE NOT c.deleted
|
||||
AND NOT c.monoforum
|
||||
AND (c.broadcast OR c.megagroup)
|
||||
AND (
|
||||
c.id = $1
|
||||
OR lower(COALESCE(c.username, '')) = $2
|
||||
OR p.username_lower = $2
|
||||
OR lower(c.title) LIKE '%' || $2 || '%'
|
||||
)
|
||||
ORDER BY c.updated_at DESC, c.id DESC
|
||||
LIMIT $3`, id, username, channelSearchLimit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search channels: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanChannelRows(rows)
|
||||
}
|
||||
|
||||
func (s *readStore) ListChannels(ctx context.Context, beforeUpdatedUS, beforeID int64, limit int) ([]ChannelRow, bool, error) {
|
||||
if limit <= 0 {
|
||||
limit = channelListDefaultLimit
|
||||
}
|
||||
if limit > channelListMaxLimit {
|
||||
limit = channelListMaxLimit
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about,
|
||||
COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username,
|
||||
c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted,
|
||||
c.participants_count, c.admins_count, c.kicked_count, c.banned_count,
|
||||
c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at
|
||||
FROM channels c
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id
|
||||
WHERE NOT c.deleted
|
||||
AND NOT c.monoforum
|
||||
AND (c.broadcast OR c.megagroup)
|
||||
AND ($1::bigint = 0 OR (c.updated_at, c.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint))
|
||||
ORDER BY c.updated_at DESC, c.id DESC
|
||||
LIMIT $3`, beforeUpdatedUS, beforeID, limit+1)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("list channels: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out, err := scanChannelRows(rows)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasMore := len(out) > limit
|
||||
if hasMore {
|
||||
out = out[:limit]
|
||||
}
|
||||
return out, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *readStore) ChannelDetail(ctx context.Context, channelID int64) (ChannelDetail, error) {
|
||||
var out ChannelDetail
|
||||
var raw []byte
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about,
|
||||
COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username,
|
||||
c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted,
|
||||
c.participants_count, c.admins_count, c.kicked_count, c.banned_count,
|
||||
c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at,
|
||||
row_to_json(c)::jsonb
|
||||
FROM channels c
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id
|
||||
WHERE c.id = $1
|
||||
AND NOT c.deleted
|
||||
AND NOT c.monoforum
|
||||
AND (c.broadcast OR c.megagroup)`, channelID).Scan(channelScanDestWithRaw(&out.Channel, &raw)...)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("get channel: %w", err)
|
||||
}
|
||||
out.ChannelJSON = prettyJSON(raw)
|
||||
out.AuditLogs, err = s.channelAuditLogs(ctx, channelID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type channelScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanChannelRows(rows pgx.Rows) ([]ChannelRow, error) {
|
||||
out := make([]ChannelRow, 0)
|
||||
for rows.Next() {
|
||||
var item ChannelRow
|
||||
if err := scanChannelRow(rows, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func scanChannelRow(row channelScanner, item *ChannelRow) error {
|
||||
return row.Scan(channelScanDest(item)...)
|
||||
}
|
||||
|
||||
func channelScanDest(item *ChannelRow) []any {
|
||||
return []any{
|
||||
&item.ID, &item.AccessHash, &item.CreatorUserID, &item.Title, &item.About, &item.Username,
|
||||
&item.Broadcast, &item.Megagroup, &item.Forum, &item.Monoforum, &item.Verified, &item.Deleted,
|
||||
&item.ParticipantsCount, &item.AdminsCount, &item.KickedCount, &item.BannedCount,
|
||||
&item.TopMessageID, &item.PinnedMessageID, &item.PTS, &item.Date, &item.CreatedAt, &item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func channelScanDestWithRaw(item *ChannelRow, raw *[]byte) []any {
|
||||
dest := channelScanDest(item)
|
||||
return append(dest, raw)
|
||||
}
|
||||
|
||||
func (s *readStore) ListAccounts(ctx context.Context, beforeActiveUS, beforeID int64, limit int) ([]AccountRow, bool, error) {
|
||||
if limit <= 0 {
|
||||
limit = accountListDefaultLimit
|
||||
}
|
||||
if limit > accountListMaxLimit {
|
||||
limit = accountListMaxLimit
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
WITH auth AS (
|
||||
SELECT user_id, max(active_at) AS last_active_at, count(*)::int AS device_count
|
||||
FROM authorizations
|
||||
GROUP BY user_id
|
||||
)
|
||||
SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at,
|
||||
COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified,
|
||||
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint,
|
||||
auth.last_active_at, auth.device_count,
|
||||
COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username
|
||||
FROM users u
|
||||
JOIN auth ON auth.user_id = u.id
|
||||
LEFT JOIN account_send_restrictions r ON r.user_id = u.id
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id
|
||||
WHERE NOT u.is_bot
|
||||
AND ($1::bigint = 0 OR (auth.last_active_at, u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint))
|
||||
ORDER BY auth.last_active_at DESC, u.id DESC
|
||||
LIMIT $3`, beforeActiveUS, beforeID, limit+1)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AccountRow, 0, limit+1)
|
||||
for rows.Next() {
|
||||
var item AccountRow
|
||||
if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasMore := len(out) > limit
|
||||
if hasMore {
|
||||
out = out[:limit]
|
||||
}
|
||||
return out, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *readStore) AccountDetail(ctx context.Context, userID int64) (AccountDetail, error) {
|
||||
var out AccountDetail
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at,
|
||||
u.about, u.last_seen_at, u.verified, u.support, u.is_bot,
|
||||
COALESCE(r.frozen, false), COALESCE(r.reason, ''),
|
||||
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint,
|
||||
COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username
|
||||
FROM users u
|
||||
LEFT JOIN account_send_restrictions r ON r.user_id = u.id
|
||||
LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id
|
||||
WHERE u.id = $1`, userID).Scan(
|
||||
&out.Account.ID, &out.Account.Phone, &out.Account.Username, &out.Account.FirstName, &out.Account.LastName,
|
||||
&out.Account.CreatedAt, &out.Account.UpdatedAt, &out.About, &out.LastSeenAt, &out.Verified, &out.Support, &out.Bot,
|
||||
&out.Account.Frozen, &out.Account.Reason, &out.Account.PremiumUntil, &out.Account.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("get account: %w", err)
|
||||
}
|
||||
out.Restriction, out.HasRestriction, err = s.restriction(ctx, userID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Authorizations, err = s.authorizations(ctx, userID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AuditLogs, err = s.auditLogs(ctx, userID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *readStore) restriction(ctx context.Context, userID int64) (RestrictionRow, bool, error) {
|
||||
var r RestrictionRow
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT frozen, reason, actor, command_id, updated_at
|
||||
FROM account_send_restrictions
|
||||
WHERE user_id = $1`, userID).Scan(&r.Frozen, &r.Reason, &r.Actor, &r.CommandID, &r.UpdatedAt)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return RestrictionRow{}, false, nil
|
||||
}
|
||||
return RestrictionRow{}, false, fmt.Errorf("get restriction: %w", err)
|
||||
}
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
func (s *readStore) authorizations(ctx context.Context, userID int64) ([]AuthorizationRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT auth_key_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, password_pending, created_at, active_at
|
||||
FROM authorizations
|
||||
WHERE user_id = $1
|
||||
ORDER BY active_at DESC, created_at DESC
|
||||
LIMIT 100`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list authorizations: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AuthorizationRow, 0)
|
||||
for rows.Next() {
|
||||
var a AuthorizationRow
|
||||
if err := rows.Scan(&a.AuthKeyID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion, &a.APIID, &a.AppVersion, &a.IP, &a.PasswordPending, &a.CreatedAt, &a.ActiveAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) auditLogs(ctx context.Context, userID int64) ([]AuditLogRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, command_id, actor, action, dry_run, reason, status, error, result, created_at
|
||||
FROM admin_audit_logs
|
||||
WHERE target_user_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT 30`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AuditLogRow, 0)
|
||||
for rows.Next() {
|
||||
var a AuditLogRow
|
||||
var result []byte
|
||||
if err := rows.Scan(&a.ID, &a.CommandID, &a.Actor, &a.Action, &a.DryRun, &a.Reason, &a.Status, &a.Error, &result, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.Result = prettyJSON(result)
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) channelAuditLogs(ctx context.Context, channelID int64) ([]AuditLogRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, command_id, actor, action, dry_run, reason, status, error, result, created_at
|
||||
FROM admin_audit_logs
|
||||
WHERE target_peer_type = 'channel' AND target_peer_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT 30`, channelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list channel audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AuditLogRow, 0)
|
||||
for rows.Next() {
|
||||
var a AuditLogRow
|
||||
var result []byte
|
||||
if err := rows.Scan(&a.ID, &a.CommandID, &a.Actor, &a.Action, &a.DryRun, &a.Reason, &a.Status, &a.Error, &result, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.Result = prettyJSON(result)
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
type MessageRow struct {
|
||||
OwnerUserID int64
|
||||
BoxID int
|
||||
PrivateMessageID int64
|
||||
MessageSenderID int64
|
||||
PeerID int64
|
||||
FromUserID int64
|
||||
Date int64
|
||||
Outgoing bool
|
||||
Body string
|
||||
PTS int
|
||||
Deleted bool
|
||||
Media string
|
||||
}
|
||||
|
||||
type GroupMessageRow struct {
|
||||
ChannelID int64
|
||||
ID int
|
||||
SenderUserID int64
|
||||
FromPeerType string
|
||||
FromPeerID int64
|
||||
Date int64
|
||||
Post bool
|
||||
Body string
|
||||
PTS int
|
||||
Deleted bool
|
||||
Media string
|
||||
ViewsCount int
|
||||
EditDate int
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
func (s *readStore) ListMessages(ctx context.Context, ownerUserID, peerID int64, beforeDate int64, beforeID int, limit int) ([]MessageRow, error) {
|
||||
if limit <= 0 || limit > messagePageLimit {
|
||||
limit = messagePageLimit
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT owner_user_id, box_id, private_message_id, message_sender_id, peer_id, from_user_id,
|
||||
message_date, outgoing, body, pts, deleted, COALESCE(media, '{}'::jsonb)
|
||||
FROM message_boxes
|
||||
WHERE owner_user_id = $1 AND peer_type = 'user' AND peer_id = $2
|
||||
AND ($3::bigint = 0 OR (message_date, box_id) < ($3::bigint, $4::int))
|
||||
ORDER BY message_date DESC, box_id DESC
|
||||
LIMIT $5`, ownerUserID, peerID, beforeDate, beforeID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list messages: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]MessageRow, 0)
|
||||
for rows.Next() {
|
||||
var item MessageRow
|
||||
var media []byte
|
||||
if err := rows.Scan(&item.OwnerUserID, &item.BoxID, &item.PrivateMessageID, &item.MessageSenderID, &item.PeerID, &item.FromUserID, &item.Date, &item.Outgoing, &item.Body, &item.PTS, &item.Deleted, &media); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Media = prettyJSON(media)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) ListGroupMessages(ctx context.Context, channelID int64, beforeDate int64, beforeID int, limit int) ([]GroupMessageRow, error) {
|
||||
if limit <= 0 || limit > messagePageLimit {
|
||||
limit = messagePageLimit
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT channel_id, id, sender_user_id, from_peer_type, from_peer_id,
|
||||
message_date, post, body, pts, deleted, COALESCE(media, '{}'::jsonb),
|
||||
views_count, edit_date, pinned
|
||||
FROM channel_messages
|
||||
WHERE channel_id = $1 AND NOT deleted
|
||||
AND ($2::bigint = 0 OR (message_date, id) < ($2::bigint, $3::int))
|
||||
ORDER BY message_date DESC, id DESC
|
||||
LIMIT $4`, channelID, beforeDate, beforeID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list group messages: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]GroupMessageRow, 0)
|
||||
for rows.Next() {
|
||||
var item GroupMessageRow
|
||||
var media []byte
|
||||
if err := rows.Scan(&item.ChannelID, &item.ID, &item.SenderUserID, &item.FromPeerType, &item.FromPeerID, &item.Date, &item.Post, &item.Body, &item.PTS, &item.Deleted, &media, &item.ViewsCount, &item.EditDate, &item.Pinned); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Media = prettyJSON(media)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
type MessageDetail struct {
|
||||
Message MessageRow
|
||||
MessageJSON string
|
||||
DialogJSON string
|
||||
PrivateJSON string
|
||||
UpdateEvents []UpdateEventRow
|
||||
Outbox []OutboxRow
|
||||
}
|
||||
|
||||
type GroupMessageDetail struct {
|
||||
Message GroupMessageRow
|
||||
MessageJSON string
|
||||
ChannelJSON string
|
||||
UpdateEvents []ChannelUpdateEventRow
|
||||
}
|
||||
|
||||
type UpdateEventRow struct {
|
||||
PTS int
|
||||
PTSCount int
|
||||
Type string
|
||||
Date int64
|
||||
JSON string
|
||||
}
|
||||
|
||||
type ChannelUpdateEventRow struct {
|
||||
PTS int
|
||||
PTSCount int
|
||||
Type string
|
||||
MessageID int
|
||||
Date int64
|
||||
SenderUserID int64
|
||||
JSON string
|
||||
}
|
||||
|
||||
type OutboxRow struct {
|
||||
ID int64
|
||||
TargetUserID int64
|
||||
PTS int
|
||||
EventType string
|
||||
Status string
|
||||
Attempts int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (s *readStore) MessageDetail(ctx context.Context, ownerUserID int64, msgID int) (MessageDetail, error) {
|
||||
var out MessageDetail
|
||||
var media []byte
|
||||
var messageJSON []byte
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT owner_user_id, box_id, private_message_id, message_sender_id, peer_id, from_user_id,
|
||||
message_date, outgoing, body, pts, deleted, COALESCE(media, '{}'::jsonb), row_to_json(mb)::jsonb
|
||||
FROM message_boxes mb
|
||||
WHERE owner_user_id = $1 AND box_id = $2`, ownerUserID, msgID).Scan(
|
||||
&out.Message.OwnerUserID, &out.Message.BoxID, &out.Message.PrivateMessageID, &out.Message.MessageSenderID, &out.Message.PeerID,
|
||||
&out.Message.FromUserID, &out.Message.Date, &out.Message.Outgoing, &out.Message.Body, &out.Message.PTS, &out.Message.Deleted,
|
||||
&media, &messageJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("get message: %w", err)
|
||||
}
|
||||
out.Message.Media = prettyJSON(media)
|
||||
out.MessageJSON = prettyJSON(messageJSON)
|
||||
out.DialogJSON, _ = s.rowJSON(ctx, `SELECT row_to_json(d)::jsonb FROM dialogs d WHERE user_id = $1 AND peer_type = 'user' AND peer_id = $2`, ownerUserID, out.Message.PeerID)
|
||||
out.PrivateJSON, _ = s.rowJSON(ctx, `SELECT row_to_json(pm)::jsonb FROM private_messages pm WHERE id = $1`, out.Message.PrivateMessageID)
|
||||
events, err := s.updateEvents(ctx, ownerUserID, msgID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.UpdateEvents = events
|
||||
outbox, err := s.outbox(ctx, ownerUserID, out.Message.PTS)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Outbox = outbox
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *readStore) GroupMessageDetail(ctx context.Context, channelID int64, msgID int) (GroupMessageDetail, error) {
|
||||
var out GroupMessageDetail
|
||||
var media []byte
|
||||
var messageJSON []byte
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT channel_id, id, sender_user_id, from_peer_type, from_peer_id,
|
||||
message_date, post, body, pts, deleted, COALESCE(media, '{}'::jsonb),
|
||||
views_count, edit_date, pinned, row_to_json(cm)::jsonb
|
||||
FROM channel_messages cm
|
||||
WHERE channel_id = $1 AND id = $2`, channelID, msgID).Scan(
|
||||
&out.Message.ChannelID, &out.Message.ID, &out.Message.SenderUserID, &out.Message.FromPeerType, &out.Message.FromPeerID,
|
||||
&out.Message.Date, &out.Message.Post, &out.Message.Body, &out.Message.PTS, &out.Message.Deleted, &media,
|
||||
&out.Message.ViewsCount, &out.Message.EditDate, &out.Message.Pinned, &messageJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("get group message: %w", err)
|
||||
}
|
||||
out.Message.Media = prettyJSON(media)
|
||||
out.MessageJSON = prettyJSON(messageJSON)
|
||||
out.ChannelJSON, _ = s.rowJSON(ctx, `SELECT row_to_json(c)::jsonb FROM channels c WHERE id = $1`, channelID)
|
||||
events, err := s.channelUpdateEvents(ctx, channelID, msgID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.UpdateEvents = events
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *readStore) rowJSON(ctx context.Context, sql string, args ...any) (string, error) {
|
||||
var raw []byte
|
||||
if err := s.pool.QueryRow(ctx, sql, args...).Scan(&raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prettyJSON(raw), nil
|
||||
}
|
||||
|
||||
func (s *readStore) updateEvents(ctx context.Context, ownerUserID int64, msgID int) ([]UpdateEventRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT pts, pts_count, event_type, date, row_to_json(e)::jsonb
|
||||
FROM user_update_events e
|
||||
WHERE user_id = $1 AND (
|
||||
message_box_id = $2 OR message_ids @> $3::jsonb
|
||||
)
|
||||
ORDER BY pts DESC
|
||||
LIMIT 20`, ownerUserID, msgID, fmt.Sprintf("[%d]", msgID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list update events: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]UpdateEventRow, 0)
|
||||
for rows.Next() {
|
||||
var e UpdateEventRow
|
||||
var raw []byte
|
||||
if err := rows.Scan(&e.PTS, &e.PTSCount, &e.Type, &e.Date, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.JSON = prettyJSON(raw)
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) channelUpdateEvents(ctx context.Context, channelID int64, msgID int) ([]ChannelUpdateEventRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT pts, pts_count, event_type, message_id, date, sender_user_id, row_to_json(e)::jsonb
|
||||
FROM channel_update_events e
|
||||
WHERE channel_id = $1 AND (
|
||||
message_id = $2 OR message_ids @> $3::jsonb
|
||||
)
|
||||
ORDER BY pts DESC
|
||||
LIMIT 20`, channelID, msgID, fmt.Sprintf("[%d]", msgID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list channel update events: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]ChannelUpdateEventRow, 0)
|
||||
for rows.Next() {
|
||||
var e ChannelUpdateEventRow
|
||||
var raw []byte
|
||||
if err := rows.Scan(&e.PTS, &e.PTSCount, &e.Type, &e.MessageID, &e.Date, &e.SenderUserID, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.JSON = prettyJSON(raw)
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *readStore) outbox(ctx context.Context, targetUserID int64, pts int) ([]OutboxRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, target_user_id, pts, event_type, status, attempts, created_at, updated_at
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = $1 AND pts = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 20`, targetUserID, pts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list outbox: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]OutboxRow, 0)
|
||||
for rows.Next() {
|
||||
var row OutboxRow
|
||||
if err := rows.Scan(&row.ID, &row.TargetUserID, &row.PTS, &row.EventType, &row.Status, &row.Attempts, &row.CreatedAt, &row.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func prettyJSON(raw []byte) string {
|
||||
if len(raw) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return string(raw)
|
||||
}
|
||||
out, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return string(raw)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue