Initial open source release

This commit is contained in:
A 2026-06-04 01:37:39 +08:00
commit 74992e893f
377 changed files with 118084 additions and 0 deletions

View file

@ -0,0 +1,509 @@
package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/postgres/sqlcgen"
)
// MediaStore 用 PostgreSQL 实现 store.MediaStore媒体元数据 + blob 索引)。
type MediaStore struct {
db sqlcgen.DBTX
q *sqlcgen.Queries
}
// NewMediaStore 基于 pgx 连接池(或事务)创建 MediaStore。
func NewMediaStore(db sqlcgen.DBTX) *MediaStore {
return &MediaStore{db: db, q: sqlcgen.New(db)}
}
// bytesOrEmpty 把 nil []byte 归一为空切片,避免落入 NOT NULL bytea 列时被当作 NULL。
func bytesOrEmpty(b []byte) []byte {
if b == nil {
return []byte{}
}
return b
}
var _ store.MediaStore = (*MediaStore)(nil)
// ---- 上传分片 ----
func (s *MediaStore) SaveFilePart(ctx context.Context, part domain.UploadPart) error {
return s.q.SaveUploadPart(ctx, sqlcgen.SaveUploadPartParams{
OwnerUserID: part.OwnerUserID,
FileID: part.FileID,
Part: int32(part.Part),
TotalParts: int32(part.TotalParts),
IsBig: part.Big,
Bytes: part.Bytes,
})
}
func (s *MediaStore) LoadFileParts(ctx context.Context, ownerUserID, fileID int64) ([]domain.UploadPart, error) {
rows, err := s.q.ListUploadParts(ctx, sqlcgen.ListUploadPartsParams{OwnerUserID: ownerUserID, FileID: fileID})
if err != nil {
return nil, err
}
out := make([]domain.UploadPart, 0, len(rows))
for _, r := range rows {
out = append(out, domain.UploadPart{
OwnerUserID: ownerUserID,
FileID: fileID,
Part: int(r.Part),
TotalParts: int(r.TotalParts),
Big: r.IsBig,
Bytes: r.Bytes,
})
}
return out, nil
}
func (s *MediaStore) DeleteFileParts(ctx context.Context, ownerUserID, fileID int64) error {
return s.q.DeleteUploadParts(ctx, sqlcgen.DeleteUploadPartsParams{OwnerUserID: ownerUserID, FileID: fileID})
}
// ---- blob 索引 ----
func (s *MediaStore) PutFileBlob(ctx context.Context, blob domain.FileBlob) error {
backend := string(blob.Backend)
if backend == "" {
backend = string(domain.MediaBackendLocalFS)
}
sha := blob.SHA256
if sha == nil {
sha = []byte{} // 列为 NOT NULLnil []byte 会被 pgx 当作 NULL。
}
return s.q.PutFileBlob(ctx, sqlcgen.PutFileBlobParams{
LocationKey: blob.LocationKey,
Backend: backend,
ObjectKey: blob.ObjectKey,
Size: blob.Size,
Sha256: sha,
MimeType: blob.MimeType,
})
}
func (s *MediaStore) GetFileBlob(ctx context.Context, locationKey string) (domain.FileBlob, bool, error) {
row, err := s.q.GetFileBlob(ctx, locationKey)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.FileBlob{}, false, nil
}
return domain.FileBlob{}, false, err
}
return domain.FileBlob{
LocationKey: row.LocationKey,
Backend: domain.MediaBackend(row.Backend),
ObjectKey: row.ObjectKey,
Size: row.Size,
SHA256: row.Sha256,
MimeType: row.MimeType,
}, true, nil
}
// ---- 文档 ----
func (s *MediaStore) PutDocument(ctx context.Context, doc domain.Document) error {
attrs, err := jsonArrayOrEmpty(doc.Attributes)
if err != nil {
return err
}
thumbs, err := jsonArrayOrEmpty(doc.Thumbs)
if err != nil {
return err
}
return s.q.PutDocument(ctx, sqlcgen.PutDocumentParams{
ID: doc.ID,
AccessHash: doc.AccessHash,
FileReference: bytesOrEmpty(doc.FileReference),
Date: int32(doc.Date),
MimeType: doc.MimeType,
Size: doc.Size,
DcID: int32(doc.DCID),
AttributesJson: attrs,
ThumbsJson: thumbs,
})
}
func (s *MediaStore) GetDocument(ctx context.Context, id int64) (domain.Document, bool, error) {
row, err := s.q.GetDocument(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Document{}, false, nil
}
return domain.Document{}, false, err
}
doc, err := documentFromRow(row)
if err != nil {
return domain.Document{}, false, err
}
return doc, true, nil
}
func (s *MediaStore) GetDocuments(ctx context.Context, ids []int64) ([]domain.Document, error) {
if len(ids) == 0 {
return nil, nil
}
rows, err := s.q.GetDocuments(ctx, ids)
if err != nil {
return nil, err
}
out := make([]domain.Document, 0, len(rows))
for _, r := range rows {
doc, err := documentFromRow(sqlcgen.GetDocumentRow(r))
if err != nil {
return nil, err
}
out = append(out, doc)
}
return out, nil
}
func documentFromRow(row sqlcgen.GetDocumentRow) (domain.Document, error) {
attrs, err := decodeDocumentAttributes(row.AttributesJson)
if err != nil {
return domain.Document{}, err
}
thumbs, err := decodePhotoSizes(row.ThumbsJson)
if err != nil {
return domain.Document{}, err
}
return domain.Document{
ID: row.ID,
AccessHash: row.AccessHash,
FileReference: row.FileReference,
Date: int(row.Date),
MimeType: row.MimeType,
Size: row.Size,
DCID: int(row.DcID),
Attributes: attrs,
Thumbs: thumbs,
}, nil
}
// ---- 照片 ----
func (s *MediaStore) PutPhoto(ctx context.Context, photo domain.Photo) error {
sizes, err := jsonArrayOrEmpty(photo.Sizes)
if err != nil {
return err
}
return s.q.PutPhoto(ctx, sqlcgen.PutPhotoParams{
ID: photo.ID,
AccessHash: photo.AccessHash,
FileReference: bytesOrEmpty(photo.FileReference),
Date: int32(photo.Date),
DcID: int32(photo.DCID),
HasStickers: photo.HasStickers,
SizesJson: sizes,
})
}
func (s *MediaStore) GetPhoto(ctx context.Context, id int64) (domain.Photo, bool, error) {
row, err := s.q.GetPhoto(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Photo{}, false, nil
}
return domain.Photo{}, false, err
}
sizes, err := decodePhotoSizes(row.SizesJson)
if err != nil {
return domain.Photo{}, false, err
}
return domain.Photo{
ID: row.ID,
AccessHash: row.AccessHash,
FileReference: row.FileReference,
Date: int(row.Date),
DCID: int(row.DcID),
HasStickers: row.HasStickers,
Sizes: sizes,
}, true, nil
}
// ---- 贴纸集 ----
func (s *MediaStore) PutStickerSet(ctx context.Context, set domain.StickerSet) error {
thumbs, err := jsonArrayOrEmpty(set.Thumbs)
if err != nil {
return err
}
docIDs, err := jsonArrayOrEmpty(set.DocumentIDs)
if err != nil {
return err
}
packs, err := jsonArrayOrEmpty(set.Packs)
if err != nil {
return err
}
kind := string(set.Kind)
if kind == "" {
kind = string(domain.StickerSetKindStickers)
}
return s.q.PutStickerSet(ctx, sqlcgen.PutStickerSetParams{
ID: set.ID,
AccessHash: set.AccessHash,
ShortName: set.ShortName,
Title: set.Title,
Count: int32(set.Count),
Hash: int32(set.Hash),
SetKind: kind,
Official: set.Official,
Animated: set.Animated,
Videos: set.Videos,
Emojis: set.Emojis,
Masks: set.Masks,
Installed: set.Installed,
Archived: set.Archived,
InstalledDate: int32(set.InstalledDate),
ThumbDocumentID: set.ThumbDocumentID,
ThumbsJson: thumbs,
ThumbDcID: int32(set.ThumbDCID),
ThumbVersion: int32(set.ThumbVersion),
DocumentIdsJson: docIDs,
PacksJson: packs,
SortOrder: int32(set.SortOrder),
SystemKey: set.SystemKey,
})
}
func (s *MediaStore) GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return stickerSetFromRow(row)
}
func (s *MediaStore) GetStickerSetByShortName(ctx context.Context, shortName string) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetByShortName(ctx, shortName)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
}
func (s *MediaStore) GetStickerSetBySystemKey(ctx context.Context, systemKey string) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetBySystemKey(ctx, systemKey)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
}
func (s *MediaStore) ListStickerSets(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error) {
rows, err := s.q.ListStickerSetsByKind(ctx, string(kind))
if err != nil {
return nil, err
}
out := make([]domain.StickerSet, 0, len(rows))
for _, r := range rows {
set, _, err := stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(r))
if err != nil {
return nil, err
}
out = append(out, set)
}
return out, nil
}
func (s *MediaStore) CountStickerSets(ctx context.Context) (int, error) {
n, err := s.q.CountStickerSets(ctx)
return int(n), err
}
func stickerSetFromRow(row sqlcgen.GetStickerSetByIDRow) (domain.StickerSet, bool, error) {
thumbs, err := decodePhotoSizes(row.ThumbsJson)
if err != nil {
return domain.StickerSet{}, false, err
}
docIDs, err := decodeInt64Slice(row.DocumentIdsJson)
if err != nil {
return domain.StickerSet{}, false, err
}
packs, err := decodeStickerPacks(row.PacksJson)
if err != nil {
return domain.StickerSet{}, false, err
}
return domain.StickerSet{
ID: row.ID,
AccessHash: row.AccessHash,
ShortName: row.ShortName,
Title: row.Title,
Count: int(row.Count),
Hash: int(row.Hash),
Kind: domain.StickerSetKind(row.SetKind),
Official: row.Official,
Animated: row.Animated,
Videos: row.Videos,
Emojis: row.Emojis,
Masks: row.Masks,
Installed: row.Installed,
Archived: row.Archived,
InstalledDate: int(row.InstalledDate),
ThumbDocumentID: row.ThumbDocumentID,
Thumbs: thumbs,
ThumbDCID: int(row.ThumbDcID),
ThumbVersion: int(row.ThumbVersion),
DocumentIDs: docIDs,
Packs: packs,
SortOrder: int(row.SortOrder),
SystemKey: row.SystemKey,
}, true, nil
}
// ---- 可用 reaction ----
func (s *MediaStore) PutAvailableReaction(ctx context.Context, r domain.AvailableReaction) error {
return s.q.PutAvailableReaction(ctx, sqlcgen.PutAvailableReactionParams{
Reaction: r.Reaction,
Title: r.Title,
Inactive: r.Inactive,
Premium: r.Premium,
StaticIconID: r.StaticIconID,
AppearAnimationID: r.AppearAnimationID,
SelectAnimationID: r.SelectAnimationID,
ActivateAnimationID: r.ActivateAnimationID,
EffectAnimationID: r.EffectAnimationID,
AroundAnimationID: r.AroundAnimationID,
CenterIconID: r.CenterIconID,
SortOrder: int32(r.Order),
})
}
func (s *MediaStore) ListAvailableReactions(ctx context.Context) ([]domain.AvailableReaction, error) {
rows, err := s.q.ListAvailableReactions(ctx)
if err != nil {
return nil, err
}
out := make([]domain.AvailableReaction, 0, len(rows))
for _, r := range rows {
out = append(out, domain.AvailableReaction{
Reaction: r.Reaction,
Title: r.Title,
Inactive: r.Inactive,
Premium: r.Premium,
StaticIconID: r.StaticIconID,
AppearAnimationID: r.AppearAnimationID,
SelectAnimationID: r.SelectAnimationID,
ActivateAnimationID: r.ActivateAnimationID,
EffectAnimationID: r.EffectAnimationID,
AroundAnimationID: r.AroundAnimationID,
CenterIconID: r.CenterIconID,
Order: int(r.SortOrder),
})
}
return out, nil
}
func (s *MediaStore) CountAvailableReactions(ctx context.Context) (int, error) {
n, err := s.q.CountAvailableReactions(ctx)
return int(n), err
}
// ---- 头像历史 ----
func (s *MediaStore) AddProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID, photoID int64, date int) error {
next, err := s.q.NextProfilePhotoOrder(ctx, sqlcgen.NextProfilePhotoOrderParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
})
if err != nil {
return err
}
return s.q.AddProfilePhoto(ctx, sqlcgen.AddProfilePhotoParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
PhotoID: photoID,
Date: int32(date),
SortOrder: next + 1,
})
}
func (s *MediaStore) CurrentProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID int64) (int64, bool, error) {
id, err := s.q.CurrentProfilePhoto(ctx, sqlcgen.CurrentProfilePhotoParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return 0, false, nil
}
return 0, false, err
}
return id, true, nil
}
func (s *MediaStore) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
if len(ownerIDs) == 0 {
return map[int64]domain.ProfilePhotoRef{}, nil
}
rows, err := s.q.CurrentProfilePhotosForOwners(ctx, sqlcgen.CurrentProfilePhotosForOwnersParams{
OwnerPeerType: string(ownerType),
OwnerIds: ownerIDs,
})
if err != nil {
return nil, err
}
out := make(map[int64]domain.ProfilePhotoRef, len(rows))
for _, r := range rows {
sizes, err := decodePhotoSizes(r.SizesJson)
if err != nil {
return nil, err
}
out[r.OwnerPeerID] = domain.ProfilePhotoRef{
PhotoID: r.PhotoID,
DCID: int(r.DcID),
Stripped: domain.StrippedFromSizes(sizes),
}
}
return out, nil
}
func (s *MediaStore) ListProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, offset, limit int, maxID int64) ([]int64, int, error) {
ids, err := s.q.ListProfilePhotos(ctx, sqlcgen.ListProfilePhotosParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
MaxID: maxID,
OffsetCount: int32(offset),
LimitCount: int32(limit),
})
if err != nil {
return nil, 0, err
}
total, err := s.q.CountProfilePhotos(ctx, sqlcgen.CountProfilePhotosParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
})
if err != nil {
return nil, 0, err
}
return ids, int(total), nil
}
func (s *MediaStore) DeleteProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) ([]int64, error) {
if len(photoIDs) == 0 {
return nil, nil
}
return s.q.DeactivateProfilePhotos(ctx, sqlcgen.DeactivateProfilePhotosParams{
OwnerPeerType: string(ownerType),
OwnerPeerID: ownerID,
PhotoIds: photoIDs,
})
}