s3 support
This commit is contained in:
parent
fa5cfaf14d
commit
03f10b66ee
53 changed files with 2796 additions and 102 deletions
|
|
@ -479,6 +479,11 @@ ORDER BY id`, channel.ID, id32)
|
|||
// 删除入口统一静默跳过(官方客户端对它禁用删除)。
|
||||
continue
|
||||
}
|
||||
// Drop this message's media_references (storage GC); orphans the
|
||||
// document/photo if this was its last live reference anywhere.
|
||||
if err := removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindChannelMessage, channelMessageRefKey(channel.ID, id)); err != nil {
|
||||
return nil, domain.ChannelUpdateEvent{}, channel, fmt.Errorf("remove deleted channel message media references: %w", err)
|
||||
}
|
||||
deleted = append(deleted, id)
|
||||
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
|
||||
ChannelID: channel.ID,
|
||||
|
|
|
|||
|
|
@ -272,6 +272,10 @@ WHERE location_key = ANY($1::text[])`, locationKeys)
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) SumFileBlobBytes(ctx context.Context) (int64, error) {
|
||||
return s.q.SumFileBlobBytes(ctx)
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetSeedState(ctx context.Context, key string) (string, bool, error) {
|
||||
var hash string
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
|
|
@ -332,6 +336,7 @@ func putDocumentParams(doc domain.Document) (sqlcgen.PutDocumentParams, error) {
|
|||
DcID: int32(doc.DCID),
|
||||
AttributesJson: attrs,
|
||||
ThumbsJson: thumbs,
|
||||
OwnerUserID: doc.OwnerUserID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -472,6 +477,20 @@ func (c *documentMetaCache) put(id int64, doc domain.Document) {
|
|||
}
|
||||
}
|
||||
|
||||
// remove evicts id, e.g. after the row is permanently deleted (storage
|
||||
// retention sweep) so a stale cache hit can't outlive the row.
|
||||
func (c *documentMetaCache) remove(id int64) {
|
||||
if c == nil || id == 0 {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if el, ok := c.m[id]; ok {
|
||||
c.ll.Remove(el)
|
||||
delete(c.m, id)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneDocument(doc domain.Document) domain.Document {
|
||||
doc.FileReference = append([]byte(nil), doc.FileReference...)
|
||||
if len(doc.Attributes) > 0 {
|
||||
|
|
@ -514,6 +533,7 @@ func documentFromRow(row sqlcgen.GetDocumentRow) (domain.Document, error) {
|
|||
DCID: int(row.DcID),
|
||||
Attributes: attrs,
|
||||
Thumbs: thumbs,
|
||||
OwnerUserID: row.OwnerUserID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -532,6 +552,7 @@ func (s *MediaStore) PutPhoto(ctx context.Context, photo domain.Photo) error {
|
|||
DcID: int32(photo.DCID),
|
||||
HasStickers: photo.HasStickers,
|
||||
SizesJson: sizes,
|
||||
OwnerUserID: photo.OwnerUserID,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -543,7 +564,7 @@ func (s *MediaStore) GetPhoto(ctx context.Context, id int64) (domain.Photo, bool
|
|||
}
|
||||
return domain.Photo{}, false, err
|
||||
}
|
||||
photo, err := photoFromFields(row.ID, row.AccessHash, row.FileReference, int(row.Date), int(row.DcID), row.HasStickers, row.SizesJson)
|
||||
photo, err := photoFromFields(row.ID, row.AccessHash, row.FileReference, int(row.Date), int(row.DcID), row.HasStickers, row.SizesJson, row.OwnerUserID)
|
||||
if err != nil {
|
||||
return domain.Photo{}, false, err
|
||||
}
|
||||
|
|
@ -572,7 +593,7 @@ func (s *MediaStore) GetPhotos(ctx context.Context, ids []int64) ([]domain.Photo
|
|||
return nil, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, access_hash, file_reference, date, dc_id, has_stickers, sizes::text
|
||||
SELECT id, access_hash, file_reference, date, dc_id, has_stickers, sizes::text, owner_user_id
|
||||
FROM photos
|
||||
WHERE id = ANY($1::bigint[])
|
||||
`, unique)
|
||||
|
|
@ -613,14 +634,15 @@ func scanPhotoRow(row photoScanner) (domain.Photo, error) {
|
|||
dcID int32
|
||||
hasStickers bool
|
||||
sizesJSON string
|
||||
ownerUserID int64
|
||||
)
|
||||
if err := row.Scan(&id, &accessHash, &fileReference, &date, &dcID, &hasStickers, &sizesJSON); err != nil {
|
||||
if err := row.Scan(&id, &accessHash, &fileReference, &date, &dcID, &hasStickers, &sizesJSON, &ownerUserID); err != nil {
|
||||
return domain.Photo{}, err
|
||||
}
|
||||
return photoFromFields(id, accessHash, fileReference, int(date), int(dcID), hasStickers, sizesJSON)
|
||||
return photoFromFields(id, accessHash, fileReference, int(date), int(dcID), hasStickers, sizesJSON, ownerUserID)
|
||||
}
|
||||
|
||||
func photoFromFields(id, accessHash int64, fileReference []byte, date, dcID int, hasStickers bool, sizesJSON string) (domain.Photo, error) {
|
||||
func photoFromFields(id, accessHash int64, fileReference []byte, date, dcID int, hasStickers bool, sizesJSON string, ownerUserID int64) (domain.Photo, error) {
|
||||
sizes, err := decodePhotoSizes(sizesJSON)
|
||||
if err != nil {
|
||||
return domain.Photo{}, err
|
||||
|
|
@ -633,6 +655,7 @@ func photoFromFields(id, accessHash int64, fileReference []byte, date, dcID int,
|
|||
DCID: dcID,
|
||||
HasStickers: hasStickers,
|
||||
Sizes: sizes,
|
||||
OwnerUserID: ownerUserID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -697,7 +720,7 @@ func (s *MediaStore) CreateStickerSet(ctx context.Context, set domain.StickerSet
|
|||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return addStickerSetMediaReferencesTx(ctx, qtx, set.ID, docs)
|
||||
})
|
||||
if err != nil {
|
||||
if stickerSetShortNameConflict(err) {
|
||||
|
|
@ -726,7 +749,13 @@ func (s *MediaStore) UpdateStickerSet(ctx context.Context, set domain.StickerSet
|
|||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
// Re-register from scratch: drops references for any document no
|
||||
// longer in the set (candidate for storage GC once orphaned long
|
||||
// enough) and refreshes the rest.
|
||||
if err := removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindStickerSet, stickerSetRefKey(set.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return addStickerSetMediaReferencesTx(ctx, qtx, set.ID, docs)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -751,8 +780,10 @@ WHERE id = $1
|
|||
if tag.RowsAffected() == 0 {
|
||||
return domain.ErrStickerSetInvalid
|
||||
}
|
||||
_, err = tx.Exec(ctx, `DELETE FROM user_sticker_sets WHERE sticker_set_id = $1`, setID)
|
||||
return err
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM user_sticker_sets WHERE sticker_set_id = $1`, setID); err != nil {
|
||||
return err
|
||||
}
|
||||
return removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindStickerSet, stickerSetRefKey(setID))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -769,11 +800,40 @@ WHERE id = $1
|
|||
if tag.RowsAffected() == 0 {
|
||||
return domain.ErrStickerSetInvalid
|
||||
}
|
||||
_, err = tx.Exec(ctx, `DELETE FROM user_sticker_sets WHERE sticker_set_id = $1`, setID)
|
||||
return err
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM user_sticker_sets WHERE sticker_set_id = $1`, setID); err != nil {
|
||||
return err
|
||||
}
|
||||
return removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindStickerSet, stickerSetRefKey(setID))
|
||||
})
|
||||
}
|
||||
|
||||
func stickerSetRefKey(setID int64) string {
|
||||
return fmt.Sprintf("stickerset:%d", setID)
|
||||
}
|
||||
|
||||
// addStickerSetMediaReferencesTx registers every document belonging to a
|
||||
// sticker set as referenced (storage GC), clearing orphaned_at on each.
|
||||
func addStickerSetMediaReferencesTx(ctx context.Context, qtx *sqlcgen.Queries, setID int64, docs []domain.Document) error {
|
||||
refKey := stickerSetRefKey(setID)
|
||||
for _, doc := range docs {
|
||||
if doc.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if err := qtx.InsertMediaReference(ctx, sqlcgen.InsertMediaReferenceParams{
|
||||
MediaKind: string(domain.MediaKindDocument),
|
||||
MediaID: doc.ID,
|
||||
RefKind: string(domain.MediaRefKindStickerSet),
|
||||
RefKey: refKey,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register sticker set media reference: %w", err)
|
||||
}
|
||||
if err := qtx.ClearDocumentOrphan(ctx, doc.ID); err != nil {
|
||||
return fmt.Errorf("clear sticker document orphan: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertStickerSet(ctx context.Context, db sqlcgen.DBTX, set domain.StickerSet) error {
|
||||
thumbs, err := jsonArrayOrEmpty(set.Thumbs)
|
||||
if err != nil {
|
||||
|
|
@ -1197,15 +1257,29 @@ func (s *MediaStore) AddProfilePhotoKind(ctx context.Context, ownerType domain.P
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(ctx, `
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
INSERT INTO profile_photos (owner_peer_type, owner_peer_id, kind, photo_id, date, active, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, true, $6)
|
||||
ON CONFLICT (owner_peer_type, owner_peer_id, kind, photo_id) DO UPDATE SET
|
||||
date = EXCLUDED.date,
|
||||
active = true,
|
||||
sort_order = EXCLUDED.sort_order
|
||||
`, string(ownerType), ownerID, string(kind), photoID, date, next+1)
|
||||
return err
|
||||
`, string(ownerType), ownerID, string(kind), photoID, date, next+1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.q.InsertMediaReference(ctx, sqlcgen.InsertMediaReferenceParams{
|
||||
MediaKind: string(domain.MediaKindPhoto),
|
||||
MediaID: photoID,
|
||||
RefKind: string(domain.MediaRefKindProfilePhoto),
|
||||
RefKey: profilePhotoRefKey(ownerType, ownerID, kind, photoID),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register profile photo reference: %w", err)
|
||||
}
|
||||
return s.q.ClearPhotoOrphan(ctx, photoID)
|
||||
}
|
||||
|
||||
func profilePhotoRefKey(ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoID int64) string {
|
||||
return fmt.Sprintf("%s:%d:kind:%s:photo:%d", ownerType, ownerID, kind, photoID)
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhotoKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (int64, bool, error) {
|
||||
|
|
@ -1336,7 +1410,7 @@ func (s *MediaStore) ListProfilePhotoDetailsKind(ctx context.Context, ownerType
|
|||
var err error
|
||||
if offset < 0 && maxID > 0 {
|
||||
rows, err = s.db.Query(ctx, `
|
||||
SELECT ph.id, ph.access_hash, ph.file_reference, ph.date, ph.dc_id, ph.has_stickers, ph.sizes::text AS sizes_json
|
||||
SELECT ph.id, ph.access_hash, ph.file_reference, ph.date, ph.dc_id, ph.has_stickers, ph.sizes::text AS sizes_json, ph.owner_user_id
|
||||
FROM profile_photos pp
|
||||
JOIN photos ph ON ph.id = pp.photo_id
|
||||
WHERE pp.owner_peer_type = $1
|
||||
|
|
@ -1352,7 +1426,7 @@ LIMIT $5
|
|||
offset = 0
|
||||
}
|
||||
rows, err = s.db.Query(ctx, `
|
||||
SELECT ph.id, ph.access_hash, ph.file_reference, ph.date, ph.dc_id, ph.has_stickers, ph.sizes::text AS sizes_json
|
||||
SELECT ph.id, ph.access_hash, ph.file_reference, ph.date, ph.dc_id, ph.has_stickers, ph.sizes::text AS sizes_json, ph.owner_user_id
|
||||
FROM profile_photos pp
|
||||
JOIN photos ph ON ph.id = pp.photo_id
|
||||
WHERE pp.owner_peer_type = $1
|
||||
|
|
@ -1429,6 +1503,19 @@ RETURNING photo_id
|
|||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range deleted {
|
||||
if err := s.q.RemoveMediaReference(ctx, sqlcgen.RemoveMediaReferenceParams{
|
||||
MediaKind: string(domain.MediaKindPhoto),
|
||||
MediaID: id,
|
||||
RefKind: string(domain.MediaRefKindProfilePhoto),
|
||||
RefKey: profilePhotoRefKey(ownerType, ownerID, kind, id),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("remove profile photo reference: %w", err)
|
||||
}
|
||||
if err := s.q.OrphanPhotoIfUnreferenced(ctx, id); err != nil {
|
||||
return nil, fmt.Errorf("orphan check profile photo: %w", err)
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import (
|
|||
// 写路径只在「创建」和「编辑改媒体」两处维护,删除靠读查询 JOIN 过滤 deleted、不在此维护。
|
||||
|
||||
// insertChannelMediaIndexTx 为一条频道消息按其媒体类别写索引行(无类别则 no-op)。
|
||||
// 同时登记该消息内嵌 document/photo 的 media_references(存储回收用),与分类
|
||||
// 索引共用同一事务。
|
||||
func insertChannelMediaIndexTx(ctx context.Context, tx pgx.Tx, channelID int64, id, date int, media *domain.MessageMedia, entities []domain.MessageEntity) error {
|
||||
for _, c := range domain.ClassifyMediaCategories(media, entities) {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
|
|
@ -22,15 +24,18 @@ ON CONFLICT (channel_id, id, category) DO NOTHING`, channelID, id, int16(c), dat
|
|||
return fmt.Errorf("insert channel media index: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return addMediaReferencesTx(ctx, tx, media, domain.MediaRefKindChannelMessage, channelMessageRefKey(channelID, id))
|
||||
}
|
||||
|
||||
// deleteChannelMediaIndexTx 清掉一条频道消息的全部索引行(编辑改媒体前先清后插)。
|
||||
// 只在 replaceChannelMediaIndexTx 内被调用;真正的消息删除不清 *_media 分类索引
|
||||
// (读时靠 JOIN deleted 过滤,见文件头注释),但仍需在此清掉 media_references,
|
||||
// 否则 replace 场景下旧媒体永远不会被判定为孤儿。
|
||||
func deleteChannelMediaIndexTx(ctx context.Context, tx pgx.Tx, channelID int64, id int) error {
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM channel_message_media WHERE channel_id = $1 AND id = $2`, channelID, id); err != nil {
|
||||
return fmt.Errorf("delete channel media index: %w", err)
|
||||
}
|
||||
return nil
|
||||
return removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindChannelMessage, channelMessageRefKey(channelID, id))
|
||||
}
|
||||
|
||||
// replaceChannelMediaIndexTx 在编辑替换媒体后重建索引行(类别可能变化)。
|
||||
|
|
@ -41,7 +46,8 @@ func replaceChannelMediaIndexTx(ctx context.Context, tx pgx.Tx, channelID int64,
|
|||
return insertChannelMediaIndexTx(ctx, tx, channelID, id, date, media, entities)
|
||||
}
|
||||
|
||||
// insertMessageBoxMediaIndexTx 为一条私聊 owner box 按其媒体类别写索引行。
|
||||
// insertMessageBoxMediaIndexTx 为一条私聊 owner box 按其媒体类别写索引行。同时登记
|
||||
// media_references(存储回收用)。
|
||||
func insertMessageBoxMediaIndexTx(ctx context.Context, tx pgx.Tx, ownerUserID, peerID int64, boxID, date int, media *domain.MessageMedia, entities []domain.MessageEntity) error {
|
||||
for _, c := range domain.ClassifyMediaCategories(media, entities) {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
|
|
@ -51,15 +57,18 @@ ON CONFLICT (owner_user_id, box_id, category) DO NOTHING`, ownerUserID, boxID, p
|
|||
return fmt.Errorf("insert message box media index: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return addMediaReferencesTx(ctx, tx, media, domain.MediaRefKindMessageBox, messageBoxRefKey(ownerUserID, boxID))
|
||||
}
|
||||
|
||||
// deleteMessageBoxMediaIndexTx 清掉一条私聊 owner box 的全部索引行。
|
||||
// deleteMessageBoxMediaIndexTx 清掉一条私聊 owner box 的全部索引行。只在
|
||||
// replaceMessageBoxMediaIndexTx 内被调用(编辑改媒体前先清后插);真正的消息
|
||||
// 删除不清 message_box_media(读时靠 JOIN deleted 过滤),但仍需在此清掉
|
||||
// media_references,否则 replace 场景下旧媒体永远不会被判定为孤儿。
|
||||
func deleteMessageBoxMediaIndexTx(ctx context.Context, tx pgx.Tx, ownerUserID int64, boxID int) error {
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM message_box_media WHERE owner_user_id = $1 AND box_id = $2`, ownerUserID, boxID); err != nil {
|
||||
return fmt.Errorf("delete message box media index: %w", err)
|
||||
}
|
||||
return nil
|
||||
return removeMediaReferencesByKeyTx(ctx, tx, domain.MediaRefKindMessageBox, messageBoxRefKey(ownerUserID, boxID))
|
||||
}
|
||||
|
||||
// replaceMessageBoxMediaIndexTx 在编辑替换媒体后重建索引行。
|
||||
|
|
|
|||
212
internal/store/postgres/media_refs.go
Normal file
212
internal/store/postgres/media_refs.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// messageBoxRefKey/channelMessageRefKey are the ref_key encodings used by
|
||||
// media_references rows registered from the private-mailbox and channel
|
||||
// message write paths, respectively. Kept as named helpers so the add and
|
||||
// remove sides can never drift apart on format.
|
||||
func messageBoxRefKey(ownerUserID int64, boxID int) string {
|
||||
return fmt.Sprintf("user:%d:box:%d", ownerUserID, boxID)
|
||||
}
|
||||
|
||||
func channelMessageRefKey(channelID int64, messageID int) string {
|
||||
return fmt.Sprintf("channel:%d:msg:%d", channelID, messageID)
|
||||
}
|
||||
|
||||
// addMediaReferencesTx registers every document/photo embedded in media as
|
||||
// referenced by refKind/refKey, clearing orphaned_at on each if it had been
|
||||
// set by an earlier removal. Must run in the same transaction as the write
|
||||
// that creates the reference (message send/edit).
|
||||
func addMediaReferencesTx(ctx context.Context, tx sqlcgen.DBTX, media *domain.MessageMedia, refKind domain.MediaRefKind, refKey string) error {
|
||||
targets := domain.ExtractMediaRefTargets(media)
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
q := sqlcgen.New(tx)
|
||||
for _, t := range targets {
|
||||
if err := q.InsertMediaReference(ctx, sqlcgen.InsertMediaReferenceParams{
|
||||
MediaKind: string(t.Kind),
|
||||
MediaID: t.ID,
|
||||
RefKind: string(refKind),
|
||||
RefKey: refKey,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("insert media reference: %w", err)
|
||||
}
|
||||
var clearErr error
|
||||
switch t.Kind {
|
||||
case domain.MediaKindDocument:
|
||||
clearErr = q.ClearDocumentOrphan(ctx, t.ID)
|
||||
case domain.MediaKindPhoto:
|
||||
clearErr = q.ClearPhotoOrphan(ctx, t.ID)
|
||||
}
|
||||
if clearErr != nil {
|
||||
return fmt.Errorf("clear media orphan: %w", clearErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeMediaReferencesByKeyTx drops every media_references row registered
|
||||
// under refKind/refKey (no need to know which document/photo ids those were
|
||||
// -- the delete finds them) and, for each one that becomes fully
|
||||
// unreferenced as a result, marks it orphaned so the storage retention
|
||||
// sweep can consider it once old enough. Must run in the same transaction
|
||||
// as the write that removes the reference (a message being soft-deleted).
|
||||
func removeMediaReferencesByKeyTx(ctx context.Context, tx sqlcgen.DBTX, refKind domain.MediaRefKind, refKey string) error {
|
||||
rows, err := tx.Query(ctx, `
|
||||
DELETE FROM media_references
|
||||
WHERE ref_kind = $1 AND ref_key = $2
|
||||
RETURNING media_kind, media_id`, string(refKind), refKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove media references: %w", err)
|
||||
}
|
||||
type removedRef struct {
|
||||
kind string
|
||||
id int64
|
||||
}
|
||||
var removed []removedRef
|
||||
for rows.Next() {
|
||||
var r removedRef
|
||||
if err := rows.Scan(&r.kind, &r.id); err != nil {
|
||||
rows.Close()
|
||||
return fmt.Errorf("scan removed media reference: %w", err)
|
||||
}
|
||||
removed = append(removed, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("remove media references: %w", err)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
q := sqlcgen.New(tx)
|
||||
for _, r := range removed {
|
||||
var orphanErr error
|
||||
switch domain.MediaKind(r.kind) {
|
||||
case domain.MediaKindDocument:
|
||||
orphanErr = q.OrphanDocumentIfUnreferenced(ctx, r.id)
|
||||
case domain.MediaKindPhoto:
|
||||
orphanErr = q.OrphanPhotoIfUnreferenced(ctx, r.id)
|
||||
}
|
||||
if orphanErr != nil {
|
||||
return fmt.Errorf("orphan check media: %w", orphanErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- storage retention sweep ----
|
||||
|
||||
// ListOrphanedDocumentIDsOlderThan returns document ids whose orphaned_at is
|
||||
// set and older than cutoff, oldest first, up to limit.
|
||||
func (s *MediaStore) ListOrphanedDocumentIDsOlderThan(ctx context.Context, cutoff time.Time, limit int) ([]int64, error) {
|
||||
if limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.q.ListOrphanedDocumentIDsOlderThan(ctx, sqlcgen.ListOrphanedDocumentIDsOlderThanParams{
|
||||
Cutoff: pgtype.Timestamptz{Time: cutoff, Valid: true},
|
||||
BatchLimit: int32(limit),
|
||||
})
|
||||
}
|
||||
|
||||
// ListOrphanedPhotoIDsOlderThan returns photo ids whose orphaned_at is set
|
||||
// and older than cutoff, oldest first, up to limit.
|
||||
func (s *MediaStore) ListOrphanedPhotoIDsOlderThan(ctx context.Context, cutoff time.Time, limit int) ([]int64, error) {
|
||||
if limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.q.ListOrphanedPhotoIDsOlderThan(ctx, sqlcgen.ListOrphanedPhotoIDsOlderThanParams{
|
||||
Cutoff: pgtype.Timestamptz{Time: cutoff, Valid: true},
|
||||
BatchLimit: int32(limit),
|
||||
})
|
||||
}
|
||||
|
||||
// CountFileBlobRefs reports how many file_blobs rows still point at
|
||||
// (backend, objectKey) -- the caller must not physically delete the object
|
||||
// from that backend while this is > 0 (content-addressed storage: the same
|
||||
// object can be shared by multiple documents/photos).
|
||||
func (s *MediaStore) CountFileBlobRefs(ctx context.Context, backend, objectKey string) (int, error) {
|
||||
n, err := s.q.CountFileBlobRefs(ctx, sqlcgen.CountFileBlobRefsParams{Backend: backend, ObjectKey: objectKey})
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// DeleteDocumentAndBlobs deletes a document row and every file_blobs row it
|
||||
// owns (main body + thumbnail variants), returning what was deleted so the
|
||||
// caller can physically remove each object from its backend once confirming
|
||||
// (via CountFileBlobRefs, after this call) no other row still needs it.
|
||||
// Assumes the document is already orphaned -- does not check references.
|
||||
func (s *MediaStore) DeleteDocumentAndBlobs(ctx context.Context, id int64) ([]domain.FileBlob, error) {
|
||||
var blobs []domain.FileBlob
|
||||
err := withTx(ctx, s.db, "delete document and blobs", func(tx pgx.Tx) error {
|
||||
qtx := s.q.WithTx(tx)
|
||||
rows, err := qtx.ListFileBlobsByLocationPrefix(ctx, sqlcgen.ListFileBlobsByLocationPrefixParams{
|
||||
ExactKey: fmt.Sprintf("doc:%d", id),
|
||||
PrefixPattern: fmt.Sprintf("doc:%d:%%", id),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("list document blobs: %w", err)
|
||||
}
|
||||
for _, r := range rows {
|
||||
blobs = append(blobs, domain.FileBlob{
|
||||
LocationKey: r.LocationKey, Backend: domain.MediaBackend(r.Backend), ObjectKey: r.ObjectKey, Size: r.Size,
|
||||
})
|
||||
if err := qtx.DeleteFileBlobRow(ctx, r.LocationKey); err != nil {
|
||||
return fmt.Errorf("delete file blob row: %w", err)
|
||||
}
|
||||
}
|
||||
if err := qtx.DeleteDocumentRow(ctx, id); err != nil {
|
||||
return fmt.Errorf("delete document row: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.documents.remove(id)
|
||||
return blobs, nil
|
||||
}
|
||||
|
||||
// DeletePhotoAndBlobs deletes a photo row and every file_blobs row it owns
|
||||
// (one per rendition size), returning what was deleted so the caller can
|
||||
// physically remove each object from its backend once confirming (via
|
||||
// CountFileBlobRefs, after this call) no other row still needs it. Assumes
|
||||
// the photo is already orphaned -- does not check references.
|
||||
func (s *MediaStore) DeletePhotoAndBlobs(ctx context.Context, id int64) ([]domain.FileBlob, error) {
|
||||
var blobs []domain.FileBlob
|
||||
err := withTx(ctx, s.db, "delete photo and blobs", func(tx pgx.Tx) error {
|
||||
qtx := s.q.WithTx(tx)
|
||||
rows, err := qtx.ListFileBlobsByLocationPrefix(ctx, sqlcgen.ListFileBlobsByLocationPrefixParams{
|
||||
ExactKey: fmt.Sprintf("photo:%d", id),
|
||||
PrefixPattern: fmt.Sprintf("photo:%d:%%", id),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("list photo blobs: %w", err)
|
||||
}
|
||||
for _, r := range rows {
|
||||
blobs = append(blobs, domain.FileBlob{
|
||||
LocationKey: r.LocationKey, Backend: domain.MediaBackend(r.Backend), ObjectKey: r.ObjectKey, Size: r.Size,
|
||||
})
|
||||
if err := qtx.DeleteFileBlobRow(ctx, r.LocationKey); err != nil {
|
||||
return fmt.Errorf("delete file blob row: %w", err)
|
||||
}
|
||||
}
|
||||
if err := qtx.DeletePhotoRow(ctx, id); err != nil {
|
||||
return fmt.Errorf("delete photo row: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return blobs, nil
|
||||
}
|
||||
109
internal/store/postgres/media_refs_integration_test.go
Normal file
109
internal/store/postgres/media_refs_integration_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestMediaReferenceOrphanTransitions proves the core storage-retention
|
||||
// safety invariant: a document's orphaned_at is set only once every
|
||||
// reference to it is gone, and cleared the instant a new one appears --
|
||||
// so the retention sweep never targets media still visible in a
|
||||
// conversation, regardless of how many places reference it.
|
||||
func TestMediaReferenceOrphanTransitions(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
s := NewMediaStore(pool)
|
||||
|
||||
const docID = int64(9100000000000000101)
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(context.Background(), `DELETE FROM media_references WHERE media_kind = 'document' AND media_id = $1`, docID)
|
||||
_, _ = pool.Exec(context.Background(), `DELETE FROM documents WHERE id = $1`, docID)
|
||||
})
|
||||
|
||||
if err := s.PutDocument(ctx, domain.Document{ID: docID, MimeType: "text/plain", Size: 10}); err != nil {
|
||||
t.Fatalf("put document: %v", err)
|
||||
}
|
||||
|
||||
media := &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Document: &domain.Document{ID: docID}}
|
||||
|
||||
// A freshly created document has no orphaned_at yet either way -- it's
|
||||
// simply unreferenced until a message send registers the first
|
||||
// reference, at which point normal tracking takes over.
|
||||
orphaned, err := documentOrphanedAt(ctx, pool, docID)
|
||||
if err != nil {
|
||||
t.Fatalf("query orphaned_at: %v", err)
|
||||
}
|
||||
if orphaned {
|
||||
t.Fatal("expected a freshly inserted document to not be marked orphaned yet")
|
||||
}
|
||||
|
||||
// Adding a reference (as if a message carrying it was sent) clears it.
|
||||
mustAddRef(t, pool, media, domain.MediaRefKindMessageBox, "user:1:box:1")
|
||||
if orphaned, err := documentOrphanedAt(ctx, pool, docID); err != nil || orphaned {
|
||||
t.Fatalf("expected referenced document to not be orphaned, orphaned=%v err=%v", orphaned, err)
|
||||
}
|
||||
|
||||
// A second, independent reference (e.g. forwarded to another box).
|
||||
mustAddRef(t, pool, media, domain.MediaRefKindMessageBox, "user:2:box:5")
|
||||
|
||||
// Removing only one of the two references must NOT orphan the document.
|
||||
mustRemoveRefsByKey(t, pool, domain.MediaRefKindMessageBox, "user:1:box:1")
|
||||
if orphaned, err := documentOrphanedAt(ctx, pool, docID); err != nil || orphaned {
|
||||
t.Fatalf("expected document with a remaining reference to survive, orphaned=%v err=%v", orphaned, err)
|
||||
}
|
||||
|
||||
// Removing the last reference orphans it.
|
||||
mustRemoveRefsByKey(t, pool, domain.MediaRefKindMessageBox, "user:2:box:5")
|
||||
if orphaned, err := documentOrphanedAt(ctx, pool, docID); err != nil || !orphaned {
|
||||
t.Fatalf("expected document with no remaining reference to be orphaned, orphaned=%v err=%v", orphaned, err)
|
||||
}
|
||||
|
||||
// A reference reappearing after orphaning (e.g. re-sent) clears it again.
|
||||
mustAddRef(t, pool, media, domain.MediaRefKindMessageBox, "user:3:box:9")
|
||||
if orphaned, err := documentOrphanedAt(ctx, pool, docID); err != nil || orphaned {
|
||||
t.Fatalf("expected re-referenced document to no longer be orphaned, orphaned=%v err=%v", orphaned, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustAddRef(t *testing.T, pool *pgxpool.Pool, media *domain.MessageMedia, refKind domain.MediaRefKind, refKey string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin tx: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if err := addMediaReferencesTx(ctx, tx, media, refKind, refKey); err != nil {
|
||||
t.Fatalf("add media reference: %v", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
t.Fatalf("commit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRemoveRefsByKey(t *testing.T, pool *pgxpool.Pool, refKind domain.MediaRefKind, refKey string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin tx: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if err := removeMediaReferencesByKeyTx(ctx, tx, refKind, refKey); err != nil {
|
||||
t.Fatalf("remove media references: %v", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
t.Fatalf("commit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func documentOrphanedAt(ctx context.Context, pool *pgxpool.Pool, id int64) (bool, error) {
|
||||
var orphaned bool
|
||||
err := pool.QueryRow(ctx, `SELECT orphaned_at IS NOT NULL FROM documents WHERE id = $1`, id).Scan(&orphaned)
|
||||
return orphaned, err
|
||||
}
|
||||
|
|
@ -174,6 +174,11 @@ func (s *MessageStore) finishDeleteMessagesTx(ctx context.Context, db sqlcgen.DB
|
|||
if row.ownerUserID == 0 || row.boxID == 0 {
|
||||
continue
|
||||
}
|
||||
// Drop this box's media_references (storage GC); orphans the
|
||||
// document/photo if this was its last live reference anywhere.
|
||||
if err := removeMediaReferencesByKeyTx(ctx, db, domain.MediaRefKindMessageBox, messageBoxRefKey(row.ownerUserID, row.boxID)); err != nil {
|
||||
return res, fmt.Errorf("remove deleted message media references: %w", err)
|
||||
}
|
||||
idsByOwner[row.ownerUserID] = append(idsByOwner[row.ownerUserID], row.boxID)
|
||||
if row.peer.ID != 0 {
|
||||
if peersByOwner[row.ownerUserID] == nil {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ WHERE location_key = sqlc.arg(location_key)::text;
|
|||
-- documents -------------------------------------------------------------------
|
||||
|
||||
-- name: PutDocument :exec
|
||||
INSERT INTO documents (id, access_hash, file_reference, date, mime_type, size, dc_id, attributes, thumbs)
|
||||
INSERT INTO documents (id, access_hash, file_reference, date, mime_type, size, dc_id, attributes, thumbs, owner_user_id)
|
||||
VALUES (
|
||||
sqlc.arg(id)::bigint,
|
||||
sqlc.arg(access_hash)::bigint,
|
||||
|
|
@ -118,8 +118,13 @@ VALUES (
|
|||
sqlc.arg(size)::bigint,
|
||||
sqlc.arg(dc_id)::int,
|
||||
sqlc.arg(attributes_json)::jsonb,
|
||||
sqlc.arg(thumbs_json)::jsonb
|
||||
sqlc.arg(thumbs_json)::jsonb,
|
||||
sqlc.arg(owner_user_id)::bigint
|
||||
)
|
||||
-- owner_user_id is intentionally NOT in the UPDATE SET list: a document id is
|
||||
-- only ever (re-)upserted by its original uploader's own request replay, and
|
||||
-- keeping the first-write owner sticky avoids any risk of a later call
|
||||
-- (e.g. a forward re-touching the row) reassigning ownership.
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
file_reference = EXCLUDED.file_reference,
|
||||
|
|
@ -133,21 +138,26 @@ ON CONFLICT (id) DO UPDATE SET
|
|||
-- name: GetDocument :one
|
||||
SELECT id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes::text AS attributes_json,
|
||||
thumbs::text AS thumbs_json
|
||||
thumbs::text AS thumbs_json,
|
||||
owner_user_id
|
||||
FROM documents
|
||||
WHERE id = sqlc.arg(id)::bigint;
|
||||
|
||||
-- name: GetDocuments :many
|
||||
SELECT id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes::text AS attributes_json,
|
||||
thumbs::text AS thumbs_json
|
||||
thumbs::text AS thumbs_json,
|
||||
owner_user_id
|
||||
FROM documents
|
||||
WHERE id = ANY(sqlc.arg(ids)::bigint[]);
|
||||
|
||||
-- name: DeleteDocumentRow :exec
|
||||
DELETE FROM documents WHERE id = sqlc.arg(id)::bigint;
|
||||
|
||||
-- photos ----------------------------------------------------------------------
|
||||
|
||||
-- name: PutPhoto :exec
|
||||
INSERT INTO photos (id, access_hash, file_reference, date, dc_id, has_stickers, sizes)
|
||||
INSERT INTO photos (id, access_hash, file_reference, date, dc_id, has_stickers, sizes, owner_user_id)
|
||||
VALUES (
|
||||
sqlc.arg(id)::bigint,
|
||||
sqlc.arg(access_hash)::bigint,
|
||||
|
|
@ -155,8 +165,10 @@ VALUES (
|
|||
sqlc.arg(date)::int,
|
||||
sqlc.arg(dc_id)::int,
|
||||
sqlc.arg(has_stickers)::boolean,
|
||||
sqlc.arg(sizes_json)::jsonb
|
||||
sqlc.arg(sizes_json)::jsonb,
|
||||
sqlc.arg(owner_user_id)::bigint
|
||||
)
|
||||
-- owner_user_id intentionally not updated on conflict, see PutDocument.
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
file_reference = EXCLUDED.file_reference,
|
||||
|
|
@ -167,10 +179,87 @@ ON CONFLICT (id) DO UPDATE SET
|
|||
|
||||
-- name: GetPhoto :one
|
||||
SELECT id, access_hash, file_reference, date, dc_id, has_stickers,
|
||||
sizes::text AS sizes_json
|
||||
sizes::text AS sizes_json,
|
||||
owner_user_id
|
||||
FROM photos
|
||||
WHERE id = sqlc.arg(id)::bigint;
|
||||
|
||||
-- name: DeletePhotoRow :exec
|
||||
DELETE FROM photos WHERE id = sqlc.arg(id)::bigint;
|
||||
|
||||
-- media_references / storage retention -----------------------------------------
|
||||
|
||||
-- name: InsertMediaReference :exec
|
||||
INSERT INTO media_references (media_kind, media_id, ref_kind, ref_key)
|
||||
VALUES (sqlc.arg(media_kind)::text, sqlc.arg(media_id)::bigint, sqlc.arg(ref_kind)::text, sqlc.arg(ref_key)::text)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: ClearDocumentOrphan :exec
|
||||
UPDATE documents SET orphaned_at = NULL
|
||||
WHERE id = sqlc.arg(media_id)::bigint AND orphaned_at IS NOT NULL;
|
||||
|
||||
-- name: ClearPhotoOrphan :exec
|
||||
UPDATE photos SET orphaned_at = NULL
|
||||
WHERE id = sqlc.arg(media_id)::bigint AND orphaned_at IS NOT NULL;
|
||||
|
||||
-- name: RemoveMediaReference :exec
|
||||
DELETE FROM media_references
|
||||
WHERE media_kind = sqlc.arg(media_kind)::text
|
||||
AND media_id = sqlc.arg(media_id)::bigint
|
||||
AND ref_kind = sqlc.arg(ref_kind)::text
|
||||
AND ref_key = sqlc.arg(ref_key)::text;
|
||||
|
||||
-- name: OrphanDocumentIfUnreferenced :exec
|
||||
UPDATE documents SET orphaned_at = now()
|
||||
WHERE id = sqlc.arg(media_id)::bigint
|
||||
AND orphaned_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM media_references WHERE media_kind = 'document' AND media_id = sqlc.arg(media_id)::bigint
|
||||
);
|
||||
|
||||
-- name: OrphanPhotoIfUnreferenced :exec
|
||||
UPDATE photos SET orphaned_at = now()
|
||||
WHERE id = sqlc.arg(media_id)::bigint
|
||||
AND orphaned_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM media_references WHERE media_kind = 'photo' AND media_id = sqlc.arg(media_id)::bigint
|
||||
);
|
||||
|
||||
-- name: ListOrphanedDocumentIDsOlderThan :many
|
||||
SELECT id FROM documents
|
||||
WHERE orphaned_at IS NOT NULL AND orphaned_at < sqlc.arg(cutoff)::timestamptz
|
||||
ORDER BY orphaned_at ASC
|
||||
LIMIT sqlc.arg(batch_limit)::int;
|
||||
|
||||
-- name: ListOrphanedPhotoIDsOlderThan :many
|
||||
SELECT id FROM photos
|
||||
WHERE orphaned_at IS NOT NULL AND orphaned_at < sqlc.arg(cutoff)::timestamptz
|
||||
ORDER BY orphaned_at ASC
|
||||
LIMIT sqlc.arg(batch_limit)::int;
|
||||
|
||||
-- name: CountFileBlobRefs :one
|
||||
SELECT COUNT(*)::int FROM file_blobs WHERE backend = sqlc.arg(backend)::text AND object_key = sqlc.arg(object_key)::text;
|
||||
|
||||
-- name: DeleteFileBlobRow :exec
|
||||
DELETE FROM file_blobs WHERE location_key = sqlc.arg(location_key)::text;
|
||||
|
||||
-- name: ListFileBlobsByLocationPrefix :many
|
||||
-- Matches a media's main blob (exact_key, e.g. "doc:123") plus every
|
||||
-- variant keyed off it (prefix_pattern, e.g. "doc:123:%" for thumbnails /
|
||||
-- "photo:456:%" for each rendition size) -- a document/photo can own
|
||||
-- multiple file_blobs rows.
|
||||
SELECT location_key, backend, object_key, size
|
||||
FROM file_blobs
|
||||
WHERE location_key = sqlc.arg(exact_key)::text
|
||||
OR location_key LIKE sqlc.arg(prefix_pattern)::text;
|
||||
|
||||
-- name: SumFileBlobBytes :one
|
||||
-- Physical bytes actually held by the blob backend (dedup-aware: identical
|
||||
-- content uploaded by different users is one row here). Used by the
|
||||
-- low-space guard's cached usage gauge and the admin panel's "physical
|
||||
-- usage" stat.
|
||||
SELECT COALESCE(SUM(size), 0)::bigint FROM file_blobs;
|
||||
|
||||
-- sticker_sets ----------------------------------------------------------------
|
||||
|
||||
-- name: PutStickerSet :exec
|
||||
|
|
|
|||
|
|
@ -48,6 +48,26 @@ func (q *Queries) AddProfilePhoto(ctx context.Context, arg AddProfilePhotoParams
|
|||
return err
|
||||
}
|
||||
|
||||
const clearDocumentOrphan = `-- name: ClearDocumentOrphan :exec
|
||||
UPDATE documents SET orphaned_at = NULL
|
||||
WHERE id = $1::bigint AND orphaned_at IS NOT NULL
|
||||
`
|
||||
|
||||
func (q *Queries) ClearDocumentOrphan(ctx context.Context, mediaID int64) error {
|
||||
_, err := q.db.Exec(ctx, clearDocumentOrphan, mediaID)
|
||||
return err
|
||||
}
|
||||
|
||||
const clearPhotoOrphan = `-- name: ClearPhotoOrphan :exec
|
||||
UPDATE photos SET orphaned_at = NULL
|
||||
WHERE id = $1::bigint AND orphaned_at IS NOT NULL
|
||||
`
|
||||
|
||||
func (q *Queries) ClearPhotoOrphan(ctx context.Context, mediaID int64) error {
|
||||
_, err := q.db.Exec(ctx, clearPhotoOrphan, mediaID)
|
||||
return err
|
||||
}
|
||||
|
||||
const countAvailableReactions = `-- name: CountAvailableReactions :one
|
||||
SELECT count(*)::int AS total FROM available_reactions
|
||||
`
|
||||
|
|
@ -59,6 +79,22 @@ func (q *Queries) CountAvailableReactions(ctx context.Context) (int32, error) {
|
|||
return total, err
|
||||
}
|
||||
|
||||
const countFileBlobRefs = `-- name: CountFileBlobRefs :one
|
||||
SELECT COUNT(*)::int FROM file_blobs WHERE backend = $1::text AND object_key = $2::text
|
||||
`
|
||||
|
||||
type CountFileBlobRefsParams struct {
|
||||
Backend string
|
||||
ObjectKey string
|
||||
}
|
||||
|
||||
func (q *Queries) CountFileBlobRefs(ctx context.Context, arg CountFileBlobRefsParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countFileBlobRefs, arg.Backend, arg.ObjectKey)
|
||||
var column_1 int32
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const countProfilePhotos = `-- name: CountProfilePhotos :one
|
||||
SELECT count(*)::int AS total
|
||||
FROM profile_photos
|
||||
|
|
@ -199,6 +235,15 @@ func (q *Queries) DeactivateProfilePhotos(ctx context.Context, arg DeactivatePro
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const deleteDocumentRow = `-- name: DeleteDocumentRow :exec
|
||||
DELETE FROM documents WHERE id = $1::bigint
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDocumentRow(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, deleteDocumentRow, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteExpiredUploadParts = `-- name: DeleteExpiredUploadParts :many
|
||||
WITH doomed AS (
|
||||
SELECT owner_user_id, file_id, part
|
||||
|
|
@ -240,6 +285,24 @@ func (q *Queries) DeleteExpiredUploadParts(ctx context.Context, arg DeleteExpire
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const deleteFileBlobRow = `-- name: DeleteFileBlobRow :exec
|
||||
DELETE FROM file_blobs WHERE location_key = $1::text
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteFileBlobRow(ctx context.Context, locationKey string) error {
|
||||
_, err := q.db.Exec(ctx, deleteFileBlobRow, locationKey)
|
||||
return err
|
||||
}
|
||||
|
||||
const deletePhotoRow = `-- name: DeletePhotoRow :exec
|
||||
DELETE FROM photos WHERE id = $1::bigint
|
||||
`
|
||||
|
||||
func (q *Queries) DeletePhotoRow(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, deletePhotoRow, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteUploadParts = `-- name: DeleteUploadParts :many
|
||||
DELETE FROM upload_parts
|
||||
WHERE owner_user_id = $1::bigint
|
||||
|
|
@ -275,7 +338,8 @@ func (q *Queries) DeleteUploadParts(ctx context.Context, arg DeleteUploadPartsPa
|
|||
const getDocument = `-- name: GetDocument :one
|
||||
SELECT id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes::text AS attributes_json,
|
||||
thumbs::text AS thumbs_json
|
||||
thumbs::text AS thumbs_json,
|
||||
owner_user_id
|
||||
FROM documents
|
||||
WHERE id = $1::bigint
|
||||
`
|
||||
|
|
@ -290,6 +354,7 @@ type GetDocumentRow struct {
|
|||
DcID int32
|
||||
AttributesJson string
|
||||
ThumbsJson string
|
||||
OwnerUserID int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetDocument(ctx context.Context, id int64) (GetDocumentRow, error) {
|
||||
|
|
@ -305,6 +370,7 @@ func (q *Queries) GetDocument(ctx context.Context, id int64) (GetDocumentRow, er
|
|||
&i.DcID,
|
||||
&i.AttributesJson,
|
||||
&i.ThumbsJson,
|
||||
&i.OwnerUserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -312,7 +378,8 @@ func (q *Queries) GetDocument(ctx context.Context, id int64) (GetDocumentRow, er
|
|||
const getDocuments = `-- name: GetDocuments :many
|
||||
SELECT id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes::text AS attributes_json,
|
||||
thumbs::text AS thumbs_json
|
||||
thumbs::text AS thumbs_json,
|
||||
owner_user_id
|
||||
FROM documents
|
||||
WHERE id = ANY($1::bigint[])
|
||||
`
|
||||
|
|
@ -327,6 +394,7 @@ type GetDocumentsRow struct {
|
|||
DcID int32
|
||||
AttributesJson string
|
||||
ThumbsJson string
|
||||
OwnerUserID int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetDocuments(ctx context.Context, ids []int64) ([]GetDocumentsRow, error) {
|
||||
|
|
@ -348,6 +416,7 @@ func (q *Queries) GetDocuments(ctx context.Context, ids []int64) ([]GetDocuments
|
|||
&i.DcID,
|
||||
&i.AttributesJson,
|
||||
&i.ThumbsJson,
|
||||
&i.OwnerUserID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -390,7 +459,8 @@ func (q *Queries) GetFileBlob(ctx context.Context, locationKey string) (GetFileB
|
|||
|
||||
const getPhoto = `-- name: GetPhoto :one
|
||||
SELECT id, access_hash, file_reference, date, dc_id, has_stickers,
|
||||
sizes::text AS sizes_json
|
||||
sizes::text AS sizes_json,
|
||||
owner_user_id
|
||||
FROM photos
|
||||
WHERE id = $1::bigint
|
||||
`
|
||||
|
|
@ -403,6 +473,7 @@ type GetPhotoRow struct {
|
|||
DcID int32
|
||||
HasStickers bool
|
||||
SizesJson string
|
||||
OwnerUserID int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetPhoto(ctx context.Context, id int64) (GetPhotoRow, error) {
|
||||
|
|
@ -416,6 +487,7 @@ func (q *Queries) GetPhoto(ctx context.Context, id int64) (GetPhotoRow, error) {
|
|||
&i.DcID,
|
||||
&i.HasStickers,
|
||||
&i.SizesJson,
|
||||
&i.OwnerUserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -686,6 +758,31 @@ func (q *Queries) GetUploadPartUsage(ctx context.Context, ownerUserID int64) (Ge
|
|||
return i, err
|
||||
}
|
||||
|
||||
const insertMediaReference = `-- name: InsertMediaReference :exec
|
||||
|
||||
INSERT INTO media_references (media_kind, media_id, ref_kind, ref_key)
|
||||
VALUES ($1::text, $2::bigint, $3::text, $4::text)
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
type InsertMediaReferenceParams struct {
|
||||
MediaKind string
|
||||
MediaID int64
|
||||
RefKind string
|
||||
RefKey string
|
||||
}
|
||||
|
||||
// media_references / storage retention -----------------------------------------
|
||||
func (q *Queries) InsertMediaReference(ctx context.Context, arg InsertMediaReferenceParams) error {
|
||||
_, err := q.db.Exec(ctx, insertMediaReference,
|
||||
arg.MediaKind,
|
||||
arg.MediaID,
|
||||
arg.RefKind,
|
||||
arg.RefKey,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const listAvailableReactions = `-- name: ListAvailableReactions :many
|
||||
SELECT
|
||||
reaction, title, inactive, premium,
|
||||
|
|
@ -728,6 +825,118 @@ func (q *Queries) ListAvailableReactions(ctx context.Context) ([]AvailableReacti
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const listFileBlobsByLocationPrefix = `-- name: ListFileBlobsByLocationPrefix :many
|
||||
SELECT location_key, backend, object_key, size
|
||||
FROM file_blobs
|
||||
WHERE location_key = $1::text
|
||||
OR location_key LIKE $2::text
|
||||
`
|
||||
|
||||
type ListFileBlobsByLocationPrefixParams struct {
|
||||
ExactKey string
|
||||
PrefixPattern string
|
||||
}
|
||||
|
||||
type ListFileBlobsByLocationPrefixRow struct {
|
||||
LocationKey string
|
||||
Backend string
|
||||
ObjectKey string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// Matches a media's main blob (exact_key, e.g. "doc:123") plus every
|
||||
// variant keyed off it (prefix_pattern, e.g. "doc:123:%" for thumbnails /
|
||||
// "photo:456:%" for each rendition size) -- a document/photo can own
|
||||
// multiple file_blobs rows.
|
||||
func (q *Queries) ListFileBlobsByLocationPrefix(ctx context.Context, arg ListFileBlobsByLocationPrefixParams) ([]ListFileBlobsByLocationPrefixRow, error) {
|
||||
rows, err := q.db.Query(ctx, listFileBlobsByLocationPrefix, arg.ExactKey, arg.PrefixPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListFileBlobsByLocationPrefixRow
|
||||
for rows.Next() {
|
||||
var i ListFileBlobsByLocationPrefixRow
|
||||
if err := rows.Scan(
|
||||
&i.LocationKey,
|
||||
&i.Backend,
|
||||
&i.ObjectKey,
|
||||
&i.Size,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listOrphanedDocumentIDsOlderThan = `-- name: ListOrphanedDocumentIDsOlderThan :many
|
||||
SELECT id FROM documents
|
||||
WHERE orphaned_at IS NOT NULL AND orphaned_at < $1::timestamptz
|
||||
ORDER BY orphaned_at ASC
|
||||
LIMIT $2::int
|
||||
`
|
||||
|
||||
type ListOrphanedDocumentIDsOlderThanParams struct {
|
||||
Cutoff pgtype.Timestamptz
|
||||
BatchLimit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListOrphanedDocumentIDsOlderThan(ctx context.Context, arg ListOrphanedDocumentIDsOlderThanParams) ([]int64, error) {
|
||||
rows, err := q.db.Query(ctx, listOrphanedDocumentIDsOlderThan, arg.Cutoff, arg.BatchLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listOrphanedPhotoIDsOlderThan = `-- name: ListOrphanedPhotoIDsOlderThan :many
|
||||
SELECT id FROM photos
|
||||
WHERE orphaned_at IS NOT NULL AND orphaned_at < $1::timestamptz
|
||||
ORDER BY orphaned_at ASC
|
||||
LIMIT $2::int
|
||||
`
|
||||
|
||||
type ListOrphanedPhotoIDsOlderThanParams struct {
|
||||
Cutoff pgtype.Timestamptz
|
||||
BatchLimit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListOrphanedPhotoIDsOlderThan(ctx context.Context, arg ListOrphanedPhotoIDsOlderThanParams) ([]int64, error) {
|
||||
rows, err := q.db.Query(ctx, listOrphanedPhotoIDsOlderThan, arg.Cutoff, arg.BatchLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listProfilePhotos = `-- name: ListProfilePhotos :many
|
||||
SELECT photo_id
|
||||
FROM profile_photos
|
||||
|
|
@ -925,6 +1134,34 @@ func (q *Queries) NextProfilePhotoOrder(ctx context.Context, arg NextProfilePhot
|
|||
return max_order, err
|
||||
}
|
||||
|
||||
const orphanDocumentIfUnreferenced = `-- name: OrphanDocumentIfUnreferenced :exec
|
||||
UPDATE documents SET orphaned_at = now()
|
||||
WHERE id = $1::bigint
|
||||
AND orphaned_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM media_references WHERE media_kind = 'document' AND media_id = $1::bigint
|
||||
)
|
||||
`
|
||||
|
||||
func (q *Queries) OrphanDocumentIfUnreferenced(ctx context.Context, mediaID int64) error {
|
||||
_, err := q.db.Exec(ctx, orphanDocumentIfUnreferenced, mediaID)
|
||||
return err
|
||||
}
|
||||
|
||||
const orphanPhotoIfUnreferenced = `-- name: OrphanPhotoIfUnreferenced :exec
|
||||
UPDATE photos SET orphaned_at = now()
|
||||
WHERE id = $1::bigint
|
||||
AND orphaned_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM media_references WHERE media_kind = 'photo' AND media_id = $1::bigint
|
||||
)
|
||||
`
|
||||
|
||||
func (q *Queries) OrphanPhotoIfUnreferenced(ctx context.Context, mediaID int64) error {
|
||||
_, err := q.db.Exec(ctx, orphanPhotoIfUnreferenced, mediaID)
|
||||
return err
|
||||
}
|
||||
|
||||
const putAvailableReaction = `-- name: PutAvailableReaction :exec
|
||||
|
||||
INSERT INTO available_reactions (
|
||||
|
|
@ -995,7 +1232,7 @@ func (q *Queries) PutAvailableReaction(ctx context.Context, arg PutAvailableReac
|
|||
|
||||
const putDocument = `-- name: PutDocument :exec
|
||||
|
||||
INSERT INTO documents (id, access_hash, file_reference, date, mime_type, size, dc_id, attributes, thumbs)
|
||||
INSERT INTO documents (id, access_hash, file_reference, date, mime_type, size, dc_id, attributes, thumbs, owner_user_id)
|
||||
VALUES (
|
||||
$1::bigint,
|
||||
$2::bigint,
|
||||
|
|
@ -1005,7 +1242,8 @@ VALUES (
|
|||
$6::bigint,
|
||||
$7::int,
|
||||
$8::jsonb,
|
||||
$9::jsonb
|
||||
$9::jsonb,
|
||||
$10::bigint
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
|
|
@ -1028,9 +1266,14 @@ type PutDocumentParams struct {
|
|||
DcID int32
|
||||
AttributesJson []byte
|
||||
ThumbsJson []byte
|
||||
OwnerUserID int64
|
||||
}
|
||||
|
||||
// documents -------------------------------------------------------------------
|
||||
// owner_user_id is intentionally NOT in the UPDATE SET list: a document id is
|
||||
// only ever (re-)upserted by its original uploader's own request replay, and
|
||||
// keeping the first-write owner sticky avoids any risk of a later call
|
||||
// (e.g. a forward re-touching the row) reassigning ownership.
|
||||
func (q *Queries) PutDocument(ctx context.Context, arg PutDocumentParams) error {
|
||||
_, err := q.db.Exec(ctx, putDocument,
|
||||
arg.ID,
|
||||
|
|
@ -1042,6 +1285,7 @@ func (q *Queries) PutDocument(ctx context.Context, arg PutDocumentParams) error
|
|||
arg.DcID,
|
||||
arg.AttributesJson,
|
||||
arg.ThumbsJson,
|
||||
arg.OwnerUserID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1089,7 +1333,7 @@ func (q *Queries) PutFileBlob(ctx context.Context, arg PutFileBlobParams) error
|
|||
|
||||
const putPhoto = `-- name: PutPhoto :exec
|
||||
|
||||
INSERT INTO photos (id, access_hash, file_reference, date, dc_id, has_stickers, sizes)
|
||||
INSERT INTO photos (id, access_hash, file_reference, date, dc_id, has_stickers, sizes, owner_user_id)
|
||||
VALUES (
|
||||
$1::bigint,
|
||||
$2::bigint,
|
||||
|
|
@ -1097,7 +1341,8 @@ VALUES (
|
|||
$4::int,
|
||||
$5::int,
|
||||
$6::boolean,
|
||||
$7::jsonb
|
||||
$7::jsonb,
|
||||
$8::bigint
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
|
|
@ -1116,9 +1361,11 @@ type PutPhotoParams struct {
|
|||
DcID int32
|
||||
HasStickers bool
|
||||
SizesJson []byte
|
||||
OwnerUserID int64
|
||||
}
|
||||
|
||||
// photos ----------------------------------------------------------------------
|
||||
// owner_user_id intentionally not updated on conflict, see PutDocument.
|
||||
func (q *Queries) PutPhoto(ctx context.Context, arg PutPhotoParams) error {
|
||||
_, err := q.db.Exec(ctx, putPhoto,
|
||||
arg.ID,
|
||||
|
|
@ -1128,6 +1375,7 @@ func (q *Queries) PutPhoto(ctx context.Context, arg PutPhotoParams) error {
|
|||
arg.DcID,
|
||||
arg.HasStickers,
|
||||
arg.SizesJson,
|
||||
arg.OwnerUserID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1244,6 +1492,31 @@ func (q *Queries) PutStickerSet(ctx context.Context, arg PutStickerSetParams) er
|
|||
return err
|
||||
}
|
||||
|
||||
const removeMediaReference = `-- name: RemoveMediaReference :exec
|
||||
DELETE FROM media_references
|
||||
WHERE media_kind = $1::text
|
||||
AND media_id = $2::bigint
|
||||
AND ref_kind = $3::text
|
||||
AND ref_key = $4::text
|
||||
`
|
||||
|
||||
type RemoveMediaReferenceParams struct {
|
||||
MediaKind string
|
||||
MediaID int64
|
||||
RefKind string
|
||||
RefKey string
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveMediaReference(ctx context.Context, arg RemoveMediaReferenceParams) error {
|
||||
_, err := q.db.Exec(ctx, removeMediaReference,
|
||||
arg.MediaKind,
|
||||
arg.MediaID,
|
||||
arg.RefKind,
|
||||
arg.RefKey,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const saveUploadPart = `-- name: SaveUploadPart :exec
|
||||
|
||||
INSERT INTO upload_parts (owner_user_id, file_id, part, total_parts, is_big, backend, object_key, size, sha256)
|
||||
|
|
@ -1295,3 +1568,18 @@ func (q *Queries) SaveUploadPart(ctx context.Context, arg SaveUploadPartParams)
|
|||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const sumFileBlobBytes = `-- name: SumFileBlobBytes :one
|
||||
SELECT COALESCE(SUM(size), 0)::bigint FROM file_blobs
|
||||
`
|
||||
|
||||
// Physical bytes actually held by the blob backend (dedup-aware: identical
|
||||
// content uploaded by different users is one row here). Used by the
|
||||
// low-space guard's cached usage gauge and the admin panel's "physical
|
||||
// usage" stat.
|
||||
func (q *Queries) SumFileBlobBytes(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, sumFileBlobBytes)
|
||||
var column_1 int64
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,34 @@ type AccountPrivacyRule struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AccountRating struct {
|
||||
UserID int64
|
||||
Level int32
|
||||
Stars int64
|
||||
CurrentLevelStars int64
|
||||
NextLevelStars *int64
|
||||
StarsComponent int64
|
||||
ActivityComponent int64
|
||||
PenaltyComponent int64
|
||||
ManualComponent int64
|
||||
PendingStars int64
|
||||
PendingDate pgtype.Timestamptz
|
||||
ComputedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Version int64
|
||||
}
|
||||
|
||||
type AccountRatingEvent struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Kind string
|
||||
Amount int64
|
||||
Reason string
|
||||
Actor string
|
||||
CommandKey *string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AccountReactionSetting struct {
|
||||
UserID int64
|
||||
MessagesNotifyFrom string
|
||||
|
|
@ -218,6 +246,21 @@ type AttachMenuUserState struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AuthDeliveryReport struct {
|
||||
ID int64
|
||||
AuthKeyID []byte
|
||||
SessionID int64
|
||||
ClientType string
|
||||
PhoneHash []byte
|
||||
CodeHash []byte
|
||||
IssuedUserID int64
|
||||
DeliveryID string
|
||||
Channel string
|
||||
Mnc string
|
||||
Fingerprint []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
AuthKeyID int64
|
||||
Body []byte
|
||||
|
|
@ -448,6 +491,20 @@ type BotUserPermission struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type BotVerifierSetting struct {
|
||||
BotID int64
|
||||
IconDocumentID int64
|
||||
CompanyName string
|
||||
DefaultDescription string
|
||||
CanModifyCustomDescription bool
|
||||
Enabled bool
|
||||
GrantedBy string
|
||||
GrantReason string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Version int64
|
||||
}
|
||||
|
||||
type BusinessAutomationDelivery struct {
|
||||
OwnerUserID int64
|
||||
PeerUserID int64
|
||||
|
|
@ -577,6 +634,18 @@ type ChannelAdminLogEvent struct {
|
|||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ChannelAntispamDecision struct {
|
||||
ID int64
|
||||
ChannelID int64
|
||||
MessageID int32
|
||||
AuthorUserID int64
|
||||
EvidenceSchemaVersion int16
|
||||
Evidence []byte
|
||||
EvidenceHash []byte
|
||||
ReportID *int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ChannelBoostSlot struct {
|
||||
UserID int64
|
||||
Slot int32
|
||||
|
|
@ -689,25 +758,27 @@ type ChannelMediaCategoryCount struct {
|
|||
}
|
||||
|
||||
type ChannelMember struct {
|
||||
ChannelID int64
|
||||
UserID int64
|
||||
InviterUserID int64
|
||||
Role string
|
||||
Status string
|
||||
JoinedAt int32
|
||||
LeftAt int32
|
||||
AdminRights []byte
|
||||
BannedRights []byte
|
||||
Rank string
|
||||
AvailableMinID int32
|
||||
AvailableMinPts int32
|
||||
ReadInboxMaxID int32
|
||||
ReadInboxDate int32
|
||||
ReadOutboxMaxID int32
|
||||
UnreadMark bool
|
||||
SlowmodeLastSendDate int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
ChannelID int64
|
||||
UserID int64
|
||||
InviterUserID int64
|
||||
Role string
|
||||
Status string
|
||||
JoinedAt int32
|
||||
LeftAt int32
|
||||
AdminRights []byte
|
||||
BannedRights []byte
|
||||
Rank string
|
||||
AvailableMinID int32
|
||||
AvailableMinPts int32
|
||||
ReadInboxMaxID int32
|
||||
ReadInboxDate int32
|
||||
ReadOutboxMaxID int32
|
||||
UnreadMark bool
|
||||
SlowmodeLastSendDate int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
HistoryClearAnchorID int32
|
||||
HistoryClearAnchorDate int32
|
||||
}
|
||||
|
||||
type ChannelMessage struct {
|
||||
|
|
@ -910,6 +981,55 @@ type ChatlistMembership struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ClientTelemetryEvent struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Kind string
|
||||
PeerType string
|
||||
PeerID int64
|
||||
SubjectIds []int64
|
||||
Payload []byte
|
||||
Fingerprint []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CollectibleUsername struct {
|
||||
ID int64
|
||||
Username string
|
||||
UsernameLower string
|
||||
Status string
|
||||
OwnerPeerType string
|
||||
OwnerPeerID int64
|
||||
PurchaseDate pgtype.Timestamptz
|
||||
Currency string
|
||||
Amount int64
|
||||
CryptoCurrency string
|
||||
CryptoAmount int64
|
||||
Url string
|
||||
OriginalOwnerPeerType string
|
||||
OriginalOwnerPeerID int64
|
||||
TransferCount int32
|
||||
Version int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type CollectibleUsernameTransfer struct {
|
||||
ID int64
|
||||
CollectibleID int64
|
||||
Kind string
|
||||
FromPeerType string
|
||||
FromPeerID int64
|
||||
ToPeerType string
|
||||
ToPeerID int64
|
||||
Currency string
|
||||
Amount int64
|
||||
Actor string
|
||||
Reason string
|
||||
CommandKey *string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Community struct {
|
||||
ID int64
|
||||
AccessHash int64
|
||||
|
|
@ -1008,6 +1128,41 @@ type CountryCode struct {
|
|||
OrderIndex int32
|
||||
}
|
||||
|
||||
type CustomVerification struct {
|
||||
ID int64
|
||||
VerifierBotID int64
|
||||
PeerType string
|
||||
PeerID int64
|
||||
IconDocumentID int64
|
||||
Description string
|
||||
GrantedByUserID int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Version int64
|
||||
}
|
||||
|
||||
type CustomVerificationRequest struct {
|
||||
ID int64
|
||||
VerifierBotID int64
|
||||
ApplicantUserID int64
|
||||
PeerType string
|
||||
PeerID int64
|
||||
PeerTitle string
|
||||
PeerUsername string
|
||||
Reason string
|
||||
RequestedDescription string
|
||||
Status string
|
||||
DecidedBy string
|
||||
DecisionReason string
|
||||
InternalNote string
|
||||
CorrelationID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
ApprovedAt pgtype.Timestamptz
|
||||
RejectedAt pgtype.Timestamptz
|
||||
Version int64
|
||||
}
|
||||
|
||||
type Dialog struct {
|
||||
UserID int64
|
||||
PeerType string
|
||||
|
|
@ -1095,6 +1250,8 @@ type Document struct {
|
|||
Attributes []byte
|
||||
Thumbs []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
OwnerUserID int64
|
||||
OrphanedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type EncryptedFile struct {
|
||||
|
|
@ -1286,6 +1443,14 @@ type LoginCodeMessageDelivery struct {
|
|||
ExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type MediaReference struct {
|
||||
MediaKind string
|
||||
MediaID int64
|
||||
RefKind string
|
||||
RefKey string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type MessageBox struct {
|
||||
OwnerUserID int64
|
||||
BoxID int32
|
||||
|
|
@ -1343,6 +1508,126 @@ type MessageBoxMedium struct {
|
|||
MessageDate int32
|
||||
}
|
||||
|
||||
type ModerationAction struct {
|
||||
ID int64
|
||||
CaseID int64
|
||||
DecisionID int64
|
||||
Kind string
|
||||
Payload []byte
|
||||
Status string
|
||||
Attempts int32
|
||||
AvailableAt pgtype.Timestamptz
|
||||
LeaseUntil pgtype.Timestamptz
|
||||
LastError string
|
||||
CommandID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationAppeal struct {
|
||||
ID int64
|
||||
CaseID int64
|
||||
AppellantUserID int64
|
||||
AppealText string
|
||||
TextHash []byte
|
||||
Fingerprint []byte
|
||||
Status string
|
||||
PreviousCaseStatus string
|
||||
Reviewer string
|
||||
ReviewReason string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ReviewedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationAppealLink struct {
|
||||
ID int64
|
||||
CaseID int64
|
||||
AppellantUserID int64
|
||||
TokenHash []byte
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
AppealID *int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ConsumedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationCase struct {
|
||||
ID int64
|
||||
TargetPeerType string
|
||||
TargetPeerID int64
|
||||
Status string
|
||||
Severity int16
|
||||
AssignedTo string
|
||||
Version int64
|
||||
ReportCount int32
|
||||
DistinctReporterCount int32
|
||||
FirstReportAt pgtype.Timestamptz
|
||||
LastReportAt pgtype.Timestamptz
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationCaseReport struct {
|
||||
CaseID int64
|
||||
ReportID int64
|
||||
AttachedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationDecision struct {
|
||||
ID int64
|
||||
CaseID int64
|
||||
AppealID *int64
|
||||
Kind string
|
||||
Actor string
|
||||
Reason string
|
||||
CommandID string
|
||||
Fingerprint []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationLegacyEphemeralMigration struct {
|
||||
LegacyReportID int64
|
||||
ModerationReportID int64
|
||||
MigratedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationMediaHold struct {
|
||||
ReportID int64
|
||||
ItemOrdinal int16
|
||||
MediaKind string
|
||||
StorageKey string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ReleasedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationReport struct {
|
||||
ID int64
|
||||
ReporterUserID int64
|
||||
Source string
|
||||
TargetPeerType string
|
||||
TargetPeerID int64
|
||||
Reason string
|
||||
ReportOption string
|
||||
ReportComment string
|
||||
CommentHash []byte
|
||||
Fingerprint []byte
|
||||
TaxonomyVersion int16
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ModerationReportItem struct {
|
||||
ReportID int64
|
||||
Ordinal int16
|
||||
ItemKind string
|
||||
PeerType string
|
||||
PeerID int64
|
||||
ItemID int64
|
||||
SecondaryID int64
|
||||
AuthorUserID int64
|
||||
EvidenceSchemaVersion int16
|
||||
Evidence []byte
|
||||
EvidenceHash []byte
|
||||
}
|
||||
|
||||
type NotifySetting struct {
|
||||
OwnerUserID int64
|
||||
ScopeKind string
|
||||
|
|
@ -1412,6 +1697,11 @@ type PeerUsername struct {
|
|||
PeerType string
|
||||
PeerID int64
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Username string
|
||||
Active bool
|
||||
Editable bool
|
||||
SortOrder int32
|
||||
CollectibleID *int64
|
||||
}
|
||||
|
||||
type Photo struct {
|
||||
|
|
@ -1423,6 +1713,8 @@ type Photo struct {
|
|||
HasStickers bool
|
||||
Sizes []byte
|
||||
CreatedAt pgtype.Timestamptz
|
||||
OwnerUserID int64
|
||||
OrphanedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Poll struct {
|
||||
|
|
@ -1518,6 +1810,24 @@ type PrivateMessageReaction struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type PrivateNoForwardsChat struct {
|
||||
UserLowID int64
|
||||
UserHighID int64
|
||||
EnabledByUserID *int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type PrivateNoForwardsRequest struct {
|
||||
PrivateMessageSenderUserID int64
|
||||
PrivateMessageID int64
|
||||
RequesterUserID int64
|
||||
ResponderUserID int64
|
||||
ExpiresAt int32
|
||||
HandledAt int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ProfilePhoto struct {
|
||||
OwnerPeerType string
|
||||
OwnerPeerID int64
|
||||
|
|
@ -1568,6 +1878,16 @@ type SavedDialogPin struct {
|
|||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type SavedMessageReactionTag struct {
|
||||
UserID int64
|
||||
MessageBoxID int32
|
||||
ReactionType string
|
||||
ReactionValue string
|
||||
ChosenOrder int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type SavedMusic struct {
|
||||
UserID int64
|
||||
DocumentID int64
|
||||
|
|
@ -1645,6 +1965,21 @@ type SeedState struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type SponsoredMessageImpression struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
RandomIDHash []byte
|
||||
TargetPeerType string
|
||||
TargetPeerID int64
|
||||
AuthorUserID int64
|
||||
EvidenceSchemaVersion int16
|
||||
Evidence []byte
|
||||
EvidenceHash []byte
|
||||
ReportID *int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftAdminGrantCommand struct {
|
||||
RecipientUserID int64
|
||||
CommandKey string
|
||||
|
|
@ -1781,6 +2116,22 @@ type StarGiftCatalogRevision struct {
|
|||
BackgroundTextColor *int32
|
||||
}
|
||||
|
||||
// Purchase-time snapshot of per-admin channel gift notification intents; delivery uses deterministic private-message replay.
|
||||
type StarGiftChannelNotificationJob struct {
|
||||
SavedGiftID int64
|
||||
TargetUserID int64
|
||||
GiftDate int32
|
||||
Action []byte
|
||||
Attempts int32
|
||||
NextAttemptAt int32
|
||||
LeaseUntil int32
|
||||
DeliveredAt int32
|
||||
MessageID int32
|
||||
LastError string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftCollectibleBackdrop struct {
|
||||
ID int64
|
||||
CollectibleRevisionID int64
|
||||
|
|
@ -2072,7 +2423,7 @@ type StarGiftUpgradeCommand struct {
|
|||
SourceEditPts int32
|
||||
}
|
||||
|
||||
// Owner-local service-message aliases (unique outputs and separate prepaid-upgrade notifications) for one saved gift aggregate.
|
||||
// Viewer-local private service-message aliases to saved gift aggregates; the saved gift owner may be that user or an authorized channel.
|
||||
type StarGiftUserMessageRef struct {
|
||||
OwnerUserID int64
|
||||
MsgID int32
|
||||
|
|
@ -2115,6 +2466,55 @@ type StarsBalance struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarsGiveaway struct {
|
||||
ID int64
|
||||
BuyerUserID int64
|
||||
FormID int64
|
||||
ChannelID int64
|
||||
LaunchMessageID int32
|
||||
RandomID int64
|
||||
Stars int64
|
||||
Users int32
|
||||
PerUserStars int64
|
||||
YearlyBoosts int32
|
||||
UntilDate int32
|
||||
PurposeJson []byte
|
||||
State string
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarsPurchaseCommand struct {
|
||||
BuyerUserID int64
|
||||
FormID int64
|
||||
RequestFingerprint []byte
|
||||
RecipientUserID *int64
|
||||
Stars int64
|
||||
Currency string
|
||||
Amount int64
|
||||
BalanceAfter int64
|
||||
TransactionID string
|
||||
CreatedAt int32
|
||||
Kind string
|
||||
SpendPeerType *string
|
||||
SpendPeerID *int64
|
||||
PurposeJson []byte
|
||||
}
|
||||
|
||||
type StarsPurchaseForm struct {
|
||||
BuyerUserID int64
|
||||
FormID int64
|
||||
RecipientUserID *int64
|
||||
Stars int64
|
||||
Currency string
|
||||
Amount int64
|
||||
IssuedAt int32
|
||||
ExpiresAt int32
|
||||
Kind string
|
||||
SpendPeerType *string
|
||||
SpendPeerID *int64
|
||||
PurposeJson []byte
|
||||
}
|
||||
|
||||
type StarsTransaction struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
|
|
@ -2502,18 +2902,21 @@ type UserBusinessProfile struct {
|
|||
}
|
||||
|
||||
type UserChannelMemberIndex struct {
|
||||
UserID int64
|
||||
ChannelID int64
|
||||
Status string
|
||||
Megagroup bool
|
||||
Broadcast bool
|
||||
Deleted bool
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Role string
|
||||
LeftAt int32
|
||||
Forum bool
|
||||
PublicUsername bool
|
||||
CanPinMessages bool
|
||||
UserID int64
|
||||
ChannelID int64
|
||||
Status string
|
||||
Megagroup bool
|
||||
Broadcast bool
|
||||
Deleted bool
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
Role string
|
||||
LeftAt int32
|
||||
Forum bool
|
||||
PublicUsername bool
|
||||
CanPinMessages bool
|
||||
AvailableMinID int32
|
||||
HistoryClearAnchorID int32
|
||||
HistoryClearUpdatedAt int32
|
||||
}
|
||||
|
||||
type UserRecentReaction struct {
|
||||
|
|
@ -2530,6 +2933,7 @@ type UserSavedReactionTag struct {
|
|||
ReactionType string
|
||||
ReactionValue string
|
||||
Title string
|
||||
// Legacy unused column; visible counts are aggregated from saved_message_reaction_tags.
|
||||
ReactionCount int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
|
|
@ -2607,6 +3011,67 @@ type UserUpdateWatermark struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type VerificationApplication struct {
|
||||
ID int64
|
||||
ApplicantUserID int64
|
||||
TargetType string
|
||||
TargetID int64
|
||||
TargetTitle string
|
||||
TargetUsername string
|
||||
TargetAccessHash int64
|
||||
Category string
|
||||
Description string
|
||||
OfficialWebsite string
|
||||
SocialLinks []string
|
||||
PressLinks []string
|
||||
AdditionalNote string
|
||||
Status string
|
||||
ReviewerAdminID string
|
||||
DecisionReason string
|
||||
InternalNote string
|
||||
CorrelationID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
SubmittedAt pgtype.Timestamptz
|
||||
ReviewedAt pgtype.Timestamptz
|
||||
Version int64
|
||||
}
|
||||
|
||||
type VerificationApplicationEvent struct {
|
||||
ID int64
|
||||
ApplicationID int64
|
||||
Kind string
|
||||
FromStatus string
|
||||
ToStatus string
|
||||
Actor string
|
||||
Reason string
|
||||
Note string
|
||||
CorrelationID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type VerificationIcon struct {
|
||||
ID int64
|
||||
DocumentID int64
|
||||
OwnerBotID int64
|
||||
Name string
|
||||
Active bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type VerificationNotificationOutbox struct {
|
||||
ID int64
|
||||
ApplicationID int64
|
||||
RecipientUserID int64
|
||||
Kind string
|
||||
Payload []byte
|
||||
Attempts int32
|
||||
DeliveredAt pgtype.Timestamptz
|
||||
LastError string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type WebAuthorization struct {
|
||||
Hash int64
|
||||
RequestID int64
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||
}
|
||||
if status.Dirty || status.Empty || status.Version != 20260714003127 {
|
||||
t.Fatalf("migration status = %+v, want clean version 20260714003127", status)
|
||||
if status.Dirty || status.Empty || status.Version != 20260714003129 {
|
||||
t.Fatalf("migration status = %+v, want clean version 20260714003129", status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue