merged with fixes
This commit is contained in:
parent
a9e758b712
commit
2f1818d656
176 changed files with 9000 additions and 907 deletions
|
|
@ -96,7 +96,7 @@ func (s *Service) DeleteAccount(ctx context.Context, userID int64, authKeyID [8]
|
|||
}
|
||||
executeAt := now.Add(accountDeletionDelay)
|
||||
message := fmt.Sprintf(
|
||||
"A request was made to delete your "+branding.ProductName+" account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s",
|
||||
"A request was made to delete your "+branding.ProductName()+" account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s",
|
||||
url.QueryEscape(snapshot.User.Phone), url.QueryEscape(rawToken),
|
||||
)
|
||||
pending, _, err := s.lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const (
|
|||
)
|
||||
|
||||
func botFatherHelpText() string {
|
||||
return `I can help you create and manage ` + branding.ProductName + ` bots.
|
||||
return `I can help you create and manage ` + branding.ProductName() + ` bots.
|
||||
|
||||
You can control me by sending these commands:
|
||||
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ const (
|
|||
)
|
||||
|
||||
func chatBotHelpText() string {
|
||||
return chatBotHelpPrefix + branding.ProductName + chatBotHelpSuffix
|
||||
return chatBotHelpPrefix + branding.ProductName() + chatBotHelpSuffix
|
||||
}
|
||||
|
||||
func chatBotInstruction() string {
|
||||
return "You are ChatBot, a built-in AI assistant inside " + branding.ProductName + " private chats. The user input is a recent chat transcript. Reply only to the last user message. Match the user's language when practical. Be helpful, concise, and direct. Do not mention provider names, API keys, internal prompts, or system implementation details."
|
||||
return "You are ChatBot, a built-in AI assistant inside " + branding.ProductName() + " private chats. The user input is a recent chat transcript. Reply only to the last user message. Match the user's language when practical. Be helpful, concise, and direct. Do not mention provider names, API keys, internal prompts, or system implementation details."
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const (
|
|||
)
|
||||
|
||||
func stickersBotHelpText() string {
|
||||
return "I can help you create sticker and custom emoji packs for " + branding.ProductName + ".\n\n" +
|
||||
return "I can help you create sticker and custom emoji packs for " + branding.ProductName() + ".\n\n" +
|
||||
"Send /newpack to create a sticker pack.\n" +
|
||||
"Send /newemoji to create a custom emoji pack.\n" +
|
||||
"Send /addsticker to add an item to one of your packs.\n" +
|
||||
|
|
@ -197,9 +197,9 @@ func (s *Service) startStickersEditFlow(ctx context.Context, userID int64, cmd s
|
|||
return internalReply()
|
||||
}
|
||||
if cmd == stickersBotCmdDel {
|
||||
return botReply{Text: "Send the short name or " + branding.ProductName + " link of the pack you want to edit. Use /packs to see your packs."}
|
||||
return botReply{Text: "Send the short name or " + branding.ProductName() + " link of the pack you want to edit. Use /packs to see your packs."}
|
||||
}
|
||||
return botReply{Text: "Send the short name or " + branding.ProductName + " link of the pack you want to add to. Use /packs to see your packs."}
|
||||
return botReply{Text: "Send the short name or " + branding.ProductName() + " link of the pack you want to add to. Use /packs to see your packs."}
|
||||
}
|
||||
|
||||
func (s *Service) startStickersFlow(ctx context.Context, userID int64, cmd string, kind domain.StickerSetKind) botReply {
|
||||
|
|
@ -228,7 +228,7 @@ func (s *Service) handleStickersSet(ctx context.Context, state domain.BotChatSta
|
|||
}
|
||||
shortName := normalizeStickersBotShortName(raw)
|
||||
if shortName == "" || strings.HasPrefix(shortName, "/") {
|
||||
return botReply{Text: "Send the pack short name or " + branding.ProductName + " link. Use /packs to list your packs, or /cancel."}
|
||||
return botReply{Text: "Send the pack short name or " + branding.ProductName() + " link. Use /packs to list your packs, or /cancel."}
|
||||
}
|
||||
set, _, found, err := s.stickers.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByShortName, ShortName: shortName})
|
||||
if err != nil {
|
||||
|
|
@ -556,7 +556,7 @@ func (s *Service) listStickersBotPacks(ctx context.Context, userID int64) botRep
|
|||
func stickersBotStepPrompt(state domain.BotChatState) botReply {
|
||||
switch state.Step {
|
||||
case stickersBotStepSet:
|
||||
return botReply{Text: "Send the pack short name or " + branding.ProductName + " link, or /cancel."}
|
||||
return botReply{Text: "Send the pack short name or " + branding.ProductName() + " link, or /cancel."}
|
||||
case stickersBotStepTitle:
|
||||
return botReply{Text: "Send a title for this pack, or /cancel."}
|
||||
case stickersBotStepDocument:
|
||||
|
|
|
|||
|
|
@ -199,11 +199,11 @@ func verifierBotWhatText() string {
|
|||
|
||||
A third-party mark is a verifier's own icon, shown right before the name of a bot, a channel or an account, plus one line of description in its profile. It means "this verifier vouches for this peer" -- nothing more.
|
||||
|
||||
It is NOT the official ` + branding.ProductName + ` checkmark. The platform badge is granted by the platform itself (@verifybot collects those applications); a third-party mark is granted by the company running a verifier bot. The two are stored, shown and taken away separately, and neither one implies the other.`
|
||||
It is NOT the official ` + branding.ProductName() + ` checkmark. The platform badge is granted by the platform itself (@verifybot collects those applications); a third-party mark is granted by the company running a verifier bot. The two are stored, shown and taken away separately, and neither one implies the other.`
|
||||
}
|
||||
|
||||
func verifierBotHelpText() string {
|
||||
return `I am a verifier bot. I grant third-party marks: my icon before the name of your bot, channel or account, plus a description in its profile. This is not the official ` + branding.ProductName + ` checkmark.
|
||||
return `I am a verifier bot. I grant third-party marks: my icon before the name of your bot, channel or account, plus a description in its profile. This is not the official ` + branding.ProductName() + ` checkmark.
|
||||
|
||||
/start - what a third-party mark is and who grants it
|
||||
/verify - apply for the mark
|
||||
|
|
@ -1125,7 +1125,7 @@ func verifierSummaryText(state domain.BotChatState, settings domain.BotVerifierS
|
|||
b.WriteString("\n\nWhy:\n")
|
||||
b.WriteString(state.Draft[verifierDraftReason])
|
||||
b.WriteString("\n\nThis is a third-party mark, not the official ")
|
||||
b.WriteString(branding.ProductName)
|
||||
b.WriteString(branding.ProductName())
|
||||
b.WriteString(" checkmark, and I do not decide: an operator reads the application and either grants the mark or refuses it. I will message you here either way.")
|
||||
return b.String()
|
||||
}
|
||||
|
|
@ -1174,7 +1174,7 @@ func verifierDecisionText(req domain.CustomVerificationRequest) (string, bool) {
|
|||
switch req.Status {
|
||||
case domain.CustomVerificationApproved:
|
||||
return fmt.Sprintf("Application #%d is approved: %s now carries my mark -- my icon before the name and my description in the profile.\n\nThis is a third-party mark, not the official %s checkmark. Send /revoke if you ever want it removed.",
|
||||
req.ID, label, branding.ProductName), true
|
||||
req.ID, label, branding.ProductName()), true
|
||||
case domain.CustomVerificationRejected:
|
||||
text := fmt.Sprintf("Application #%d for %s was not approved, so no mark was granted.", req.ID, label)
|
||||
if reason := strings.TrimSpace(req.DecisionReason); reason != "" {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ const (
|
|||
)
|
||||
|
||||
func verifyBotStartText() string {
|
||||
return `I collect applications for official ` + branding.ProductName + ` verification: the badge shown next to the name of a channel, supergroup or bot whose identity has been confirmed.
|
||||
return `I collect applications for official ` + branding.ProductName() + ` verification: the badge shown next to the name of a channel, supergroup or bot whose identity has been confirmed.
|
||||
|
||||
Before you apply, check that the subject of the application:
|
||||
- is a channel, supergroup or bot with a public @username;
|
||||
|
|
@ -129,7 +129,7 @@ Tap the button below, or send /new, to start. Send /help for the full list of co
|
|||
}
|
||||
|
||||
func verifyBotHelpText() string {
|
||||
return `I collect official ` + branding.ProductName + ` verification applications.
|
||||
return `I collect official ` + branding.ProductName() + ` verification applications.
|
||||
|
||||
/new - file a verification application
|
||||
/status - list your applications and their status
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (s *Service) SeedDefaultVerifier(ctx context.Context) (bool, error) {
|
|||
if _, err := s.GrantVerifier(ctx, domain.BotVerifierSettings{
|
||||
BotID: domain.VerifierBotUserID,
|
||||
IconDocumentID: icon.DocumentID,
|
||||
CompanyName: branding.ProductName,
|
||||
CompanyName: branding.ProductName(),
|
||||
DefaultDescription: "Bundled reference verifier -- auto-granted on first boot.",
|
||||
CanModifyCustomDescription: false,
|
||||
Enabled: true,
|
||||
|
|
|
|||
|
|
@ -189,7 +189,9 @@ func (c *blobBytesCache) get(key string) ([]byte, bool) {
|
|||
if el, ok := c.m[key]; ok {
|
||||
c.ll.MoveToFront(el)
|
||||
entry := el.Value.(*blobBytesEntry)
|
||||
return append([]byte(nil), entry.bytes...), true
|
||||
// Cache entries are immutable after publication. GetFile returns a
|
||||
// capacity-clipped read-only view of the requested range.
|
||||
return entry.bytes, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package files
|
||||
|
||||
import "sync/atomic"
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// SpaceGuard bounds how much more may be written to the permanent blob
|
||||
// backend. LocalDiskSpaceGuard checks real OS free disk bytes;
|
||||
|
|
@ -24,6 +29,41 @@ type NoopSpaceGuard struct{}
|
|||
func (NoopSpaceGuard) Allow(int64) (bool, error) { return true, nil }
|
||||
func (NoopSpaceGuard) Usage() (int64, int64, bool) { return 0, 0, false }
|
||||
|
||||
// requireSpace maps a SpaceGuard rejection to domain.ErrStorageFull. A nil
|
||||
// guard always allows the write.
|
||||
func requireSpace(guard SpaceGuard, additional int64) error {
|
||||
if guard == nil {
|
||||
return nil
|
||||
}
|
||||
ok, err := guard.Allow(additional)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return domain.ErrStorageFull
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// capacityReader stops a streaming permanent write before the backend can
|
||||
// publish an object larger than the current capacity snapshot permits.
|
||||
type capacityReader struct {
|
||||
src io.Reader
|
||||
guard SpaceGuard
|
||||
total int64
|
||||
}
|
||||
|
||||
func (r *capacityReader) Read(p []byte) (int, error) {
|
||||
n, err := r.src.Read(p)
|
||||
if n > 0 {
|
||||
if guardErr := requireSpace(r.guard, r.total+int64(n)); guardErr != nil {
|
||||
return 0, guardErr
|
||||
}
|
||||
r.total += int64(n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// LocalDiskSpaceGuard rejects writes once cached free disk bytes fall below
|
||||
// minFreeBytes (<=0 disables the check). The free-bytes figure is
|
||||
// refreshed by DiskUsageWorker, not recomputed per call, to avoid a statfs
|
||||
|
|
|
|||
72
internal/app/files/guarded_backend.go
Normal file
72
internal/app/files/guarded_backend.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GuardedBlobBackend applies one capacity policy to every permanent write,
|
||||
// including seeds and non-upload media paths, instead of relying on individual
|
||||
// RPC handlers to remember a check.
|
||||
type GuardedBlobBackend struct {
|
||||
backend BlobBackend
|
||||
guard SpaceGuard
|
||||
}
|
||||
|
||||
func NewGuardedBlobBackend(backend BlobBackend, guard SpaceGuard) *GuardedBlobBackend {
|
||||
return &GuardedBlobBackend{backend: backend, guard: guard}
|
||||
}
|
||||
|
||||
func (g *GuardedBlobBackend) Name() string { return g.backend.Name() }
|
||||
|
||||
func (g *GuardedBlobBackend) Put(ctx context.Context, data []byte) (string, error) {
|
||||
if err := requireSpace(g.guard, int64(len(data))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return g.backend.Put(ctx, data)
|
||||
}
|
||||
|
||||
func (g *GuardedBlobBackend) PutReader(ctx context.Context, r io.Reader) (string, int64, []byte, error) {
|
||||
return g.backend.PutReader(ctx, &capacityReader{src: r, guard: g.guard})
|
||||
}
|
||||
|
||||
func (g *GuardedBlobBackend) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
return g.backend.Get(ctx, key)
|
||||
}
|
||||
|
||||
func (g *GuardedBlobBackend) GetRange(ctx context.Context, key string, offset, limit int64) ([]byte, int64, error) {
|
||||
return g.backend.GetRange(ctx, key, offset, limit)
|
||||
}
|
||||
|
||||
type GuardedUploadPartBackend struct {
|
||||
backend UploadPartBackend
|
||||
guard SpaceGuard
|
||||
}
|
||||
|
||||
func NewGuardedUploadPartBackend(backend UploadPartBackend, guard SpaceGuard) *GuardedUploadPartBackend {
|
||||
return &GuardedUploadPartBackend{backend: backend, guard: guard}
|
||||
}
|
||||
|
||||
func (g *GuardedUploadPartBackend) PutUploadPart(ctx context.Context, ownerUserID, fileID int64, part int, data []byte) (uploadPartObject, error) {
|
||||
if err := requireSpace(g.guard, int64(len(data))); err != nil {
|
||||
return uploadPartObject{}, err
|
||||
}
|
||||
return g.backend.PutUploadPart(ctx, ownerUserID, fileID, part, data)
|
||||
}
|
||||
|
||||
func (g *GuardedUploadPartBackend) GetUploadPart(ctx context.Context, key string) ([]byte, error) {
|
||||
return g.backend.GetUploadPart(ctx, key)
|
||||
}
|
||||
|
||||
func (g *GuardedUploadPartBackend) OpenUploadPart(ctx context.Context, key string) (io.ReadCloser, error) {
|
||||
return g.backend.OpenUploadPart(ctx, key)
|
||||
}
|
||||
|
||||
func (g *GuardedUploadPartBackend) DeleteUploadPart(ctx context.Context, key string) error {
|
||||
return g.backend.DeleteUploadPart(ctx, key)
|
||||
}
|
||||
|
||||
func (g *GuardedUploadPartBackend) DeleteExpiredUploadParts(ctx context.Context, before time.Time, limit int) (int64, error) {
|
||||
return g.backend.DeleteExpiredUploadParts(ctx, before, limit)
|
||||
}
|
||||
|
|
@ -188,11 +188,51 @@ func (s *Service) CreateAvatarVideoMarkupFromUpload(ctx context.Context, file do
|
|||
return s.createAvatarVideoFromUpload(ctx, file, videoStartTs, []domain.PhotoSize{markup})
|
||||
}
|
||||
|
||||
// CreateAvatarVideoFromBytes stores already-in-hand animated-video bytes as an
|
||||
// avatar Photo, for callers that skip the chunked upload.saveFilePart
|
||||
// transfer regular clients use (e.g. the admin console, which already has the
|
||||
// full file from a browser upload) -- the video counterpart of
|
||||
// CreateAvatarFromBytes.
|
||||
func (s *Service) CreateAvatarVideoFromBytes(ctx context.Context, data []byte, ownerUserID int64, videoStartTs float64) (domain.Photo, error) {
|
||||
if len(data) == 0 {
|
||||
return domain.Photo{}, domain.ErrPhotoInvalid
|
||||
}
|
||||
objectKey, size, sha256sum, err := s.blobs.PutReader(ctx, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return domain.Photo{}, err
|
||||
}
|
||||
if size == 0 {
|
||||
return domain.Photo{}, domain.ErrPhotoInvalid
|
||||
}
|
||||
body := assembledUploadBlob{ObjectKey: objectKey, Size: size, SHA256: sha256sum}
|
||||
return s.createAvatarVideoFromBlob(ctx, body, ownerUserID, videoStartTs, nil)
|
||||
}
|
||||
|
||||
func (s *Service) createAvatarVideoFromUpload(ctx context.Context, file domain.UploadedFileRef, videoStartTs float64, extraSizes []domain.PhotoSize) (domain.Photo, error) {
|
||||
body, err := s.assembleUploadBlob(ctx, file.OwnerUserID, file.FileID, file.Parts)
|
||||
if err != nil {
|
||||
return domain.Photo{}, err
|
||||
}
|
||||
photo, err := s.createAvatarVideoFromBlob(ctx, body, file.OwnerUserID, videoStartTs, extraSizes)
|
||||
if err != nil {
|
||||
return domain.Photo{}, err
|
||||
}
|
||||
if err := s.cleanupUploadParts(ctx, file.OwnerUserID, file.FileID); err != nil {
|
||||
s.log.Warn("cleanup assembled avatar video upload parts failed",
|
||||
zap.Int64("owner_user_id", file.OwnerUserID),
|
||||
zap.Int64("file_id", file.FileID),
|
||||
zap.Int64("photo_id", photo.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
// createAvatarVideoFromBlob turns an already-durable video blob (from either
|
||||
// the chunked-upload assembly path or a direct in-hand byte slice) into an
|
||||
// avatar Photo. Shared by createAvatarVideoFromUpload and
|
||||
// CreateAvatarVideoFromBytes so the still-frame extraction and photo/blob
|
||||
// record construction stay in exactly one place.
|
||||
func (s *Service) createAvatarVideoFromBlob(ctx context.Context, body assembledUploadBlob, ownerUserID int64, videoStartTs float64, extraSizes []domain.PhotoSize) (domain.Photo, error) {
|
||||
if body.Size == 0 {
|
||||
return domain.Photo{}, domain.ErrPhotoInvalid
|
||||
}
|
||||
|
|
@ -230,18 +270,11 @@ func (s *Service) createAvatarVideoFromUpload(ctx context.Context, file domain.U
|
|||
Date: int(time.Now().Unix()),
|
||||
DCID: s.dc,
|
||||
Sizes: sizes,
|
||||
OwnerUserID: file.OwnerUserID,
|
||||
OwnerUserID: ownerUserID,
|
||||
}
|
||||
if err := s.media.PutPhoto(ctx, photo); err != nil {
|
||||
return domain.Photo{}, err
|
||||
}
|
||||
if err := s.cleanupUploadParts(ctx, file.OwnerUserID, file.FileID); err != nil {
|
||||
s.log.Warn("cleanup assembled avatar video upload parts failed",
|
||||
zap.Int64("owner_user_id", file.OwnerUserID),
|
||||
zap.Int64("file_id", file.FileID),
|
||||
zap.Int64("photo_id", photoID),
|
||||
zap.Error(err))
|
||||
}
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// fakeMediaStore 是 store.MediaStore 的内存替身,用于在无 PG 时验证 seed 导入器。
|
||||
|
|
@ -29,20 +30,27 @@ type fakeMediaStore struct {
|
|||
webPages map[int64]domain.MessageWebPage
|
||||
seedState map[string]string
|
||||
receipts map[string]domain.UploadedMediaReceipt
|
||||
// profilePhotos[ownerID|kind] 保存某 owner 当前 profile/fallback 照片引用。
|
||||
profilePhotos map[string]domain.ProfilePhotoRef
|
||||
}
|
||||
|
||||
func newFakeMediaStore() *fakeMediaStore {
|
||||
return &fakeMediaStore{
|
||||
blobs: map[string]domain.FileBlob{},
|
||||
docs: map[int64]domain.Document{},
|
||||
photos: map[int64]domain.Photo{},
|
||||
sets: map[int64]domain.StickerSet{},
|
||||
parts: map[string][]domain.UploadPart{},
|
||||
seedState: map[string]string{},
|
||||
receipts: map[string]domain.UploadedMediaReceipt{},
|
||||
blobs: map[string]domain.FileBlob{},
|
||||
docs: map[int64]domain.Document{},
|
||||
photos: map[int64]domain.Photo{},
|
||||
sets: map[int64]domain.StickerSet{},
|
||||
parts: map[string][]domain.UploadPart{},
|
||||
seedState: map[string]string{},
|
||||
receipts: map[string]domain.UploadedMediaReceipt{},
|
||||
profilePhotos: map[string]domain.ProfilePhotoRef{},
|
||||
}
|
||||
}
|
||||
|
||||
func fakeProfilePhotoKey(ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) string {
|
||||
return fmt.Sprintf("%s:%d:%s", ownerType, ownerID, kind)
|
||||
}
|
||||
|
||||
func fakeUploadReceiptKey(ownerUserID, fileID int64) string {
|
||||
return fmt.Sprintf("%d/%d", ownerUserID, fileID)
|
||||
}
|
||||
|
|
@ -414,29 +422,101 @@ func (f *fakeMediaStore) CountAvailableReactions(_ context.Context) (int, error)
|
|||
defer f.mu.Unlock()
|
||||
return len(f.reactions), nil
|
||||
}
|
||||
func (f *fakeMediaStore) AddProfilePhotoKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind, _ int64, _ int) error {
|
||||
func (f *fakeMediaStore) AddProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoID int64, date int) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
key := fakeProfilePhotoKey(ownerType, ownerID, kind)
|
||||
existing := f.profilePhotos[key]
|
||||
ref := domain.ProfilePhotoRef{PhotoID: photoID}
|
||||
if p, ok := f.photos[photoID]; ok {
|
||||
ref.DCID = p.DCID
|
||||
ref.Stripped = domain.StrippedFromSizes(p.Sizes)
|
||||
ref.HasVideo = domain.PhotoHasVideo(p.Sizes)
|
||||
}
|
||||
if existing.PhotoID != photoID {
|
||||
f.profilePhotos[key] = ref
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (f *fakeMediaStore) CurrentProfilePhotoKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind) (int64, bool, error) {
|
||||
return 0, false, nil
|
||||
func (f *fakeMediaStore) CurrentProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (int64, bool, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]
|
||||
if !ok {
|
||||
return 0, false, nil
|
||||
}
|
||||
return ref.PhotoID, true, nil
|
||||
}
|
||||
func (f *fakeMediaStore) CurrentProfilePhotos(_ context.Context, _ domain.PeerType, _ []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
return map[int64]domain.ProfilePhotoRef{}, nil
|
||||
func (f *fakeMediaStore) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ids []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
return f.CurrentProfilePhotosKind(ctx, ownerType, ids, domain.ProfilePhotoKindProfile)
|
||||
}
|
||||
func (f *fakeMediaStore) CurrentProfilePhotosKind(_ context.Context, _ domain.PeerType, _ []int64, _ domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
return map[int64]domain.ProfilePhotoRef{}, nil
|
||||
func (f *fakeMediaStore) CurrentProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ids []int64, kind domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(ids))
|
||||
for _, id := range ids {
|
||||
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, id, kind)]; ok {
|
||||
out[id] = ref
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (f *fakeMediaStore) ListProfilePhotosKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind, _, _ int, _ int64) ([]int64, int, error) {
|
||||
return nil, 0, nil
|
||||
func (f *fakeMediaStore) ListProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]int64, int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var ids []int64
|
||||
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]; ok {
|
||||
ids = append(ids, ref.PhotoID)
|
||||
}
|
||||
return ids, len(ids), nil
|
||||
}
|
||||
func (f *fakeMediaStore) ListProfilePhotoDetailsKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind, _, _ int, _ int64) ([]domain.Photo, int, error) {
|
||||
return nil, 0, nil
|
||||
func (f *fakeMediaStore) ListProfilePhotoDetailsKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]domain.Photo, int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []domain.Photo
|
||||
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]; ok {
|
||||
if p, ok := f.photos[ref.PhotoID]; ok {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out, len(out), nil
|
||||
}
|
||||
func (f *fakeMediaStore) DeleteProfilePhotos(_ context.Context, _ domain.PeerType, _ int64, _ []int64) ([]int64, error) {
|
||||
return nil, nil
|
||||
func (f *fakeMediaStore) DeleteProfilePhotos(_ context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) ([]int64, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var deleted []int64
|
||||
key := fakeProfilePhotoKey(ownerType, ownerID, domain.ProfilePhotoKindProfile)
|
||||
if ref, ok := f.profilePhotos[key]; ok {
|
||||
for _, id := range photoIDs {
|
||||
if id == ref.PhotoID {
|
||||
deleted = append(deleted, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(deleted) > 0 {
|
||||
delete(f.profilePhotos, key)
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (f *fakeMediaStore) DeleteProfilePhotosKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind, _ []int64) ([]int64, error) {
|
||||
return nil, nil
|
||||
func (f *fakeMediaStore) DeleteProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoIDs []int64) ([]int64, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var deleted []int64
|
||||
key := fakeProfilePhotoKey(ownerType, ownerID, kind)
|
||||
if ref, ok := f.profilePhotos[key]; ok {
|
||||
for _, id := range photoIDs {
|
||||
if id == ref.PhotoID {
|
||||
deleted = append(deleted, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(deleted) > 0 {
|
||||
delete(f.profilePhotos, key)
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (f *fakeMediaStore) WithTx(_ context.Context, fn func(ctx context.Context, txMedia store.MediaStore) error) error {
|
||||
return fn(context.Background(), f)
|
||||
}
|
||||
|
||||
func TestSeedMediaRepairsPartialReactionBlobs(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -256,7 +256,9 @@ func TestMaxUploadFileBytesUnlimitedByDefault(t *testing.T) {
|
|||
|
||||
type countingUploadPartBackend struct {
|
||||
*LocalFS
|
||||
getUploadPartCalls int
|
||||
getUploadPartCalls int
|
||||
putUploadPartCalls int
|
||||
deleteUploadPartCalls int
|
||||
}
|
||||
|
||||
func (c *countingUploadPartBackend) GetUploadPart(ctx context.Context, objectKey string) ([]byte, error) {
|
||||
|
|
@ -264,6 +266,16 @@ func (c *countingUploadPartBackend) GetUploadPart(ctx context.Context, objectKey
|
|||
return c.LocalFS.GetUploadPart(ctx, objectKey)
|
||||
}
|
||||
|
||||
func (c *countingUploadPartBackend) PutUploadPart(ctx context.Context, ownerUserID, fileID int64, part int, data []byte) (uploadPartObject, error) {
|
||||
c.putUploadPartCalls++
|
||||
return c.LocalFS.PutUploadPart(ctx, ownerUserID, fileID, part, data)
|
||||
}
|
||||
|
||||
func (c *countingUploadPartBackend) DeleteUploadPart(ctx context.Context, objectKey string) error {
|
||||
c.deleteUploadPartCalls++
|
||||
return c.LocalFS.DeleteUploadPart(ctx, objectKey)
|
||||
}
|
||||
|
||||
func newUploadPartTestService(t *testing.T, media *fakeMediaStore, quota domain.UploadPartQuota) (*Service, *LocalFS) {
|
||||
t.Helper()
|
||||
blobs, err := NewLocalFS(t.TempDir())
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEnt
|
|||
packs: packs,
|
||||
packCache: newLangPackCache(maxBytes, maxEntries),
|
||||
languageCache: newLanguageListCache(languageEntries),
|
||||
publicBaseURL: branding.DefaultPublicURL,
|
||||
publicBaseURL: branding.PublicBaseURL(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func NewService(creds store.PasskeyStore, challenges store.PasskeyChallengeStore
|
|||
creds: creds,
|
||||
challenges: challenges,
|
||||
rpID: rpID,
|
||||
rpName: branding.ProductName,
|
||||
rpName: branding.ProductName(),
|
||||
dcID: dcID,
|
||||
challengeTTL: defaultChallengeTTL,
|
||||
now: time.Now,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import (
|
|||
// 强制重新拉取 p/g,而不是信任本地缓存。用于失效任何账号本地可能缓存的陈旧/错误
|
||||
// p/g(例如账号早年间对接过其它后端、缓存版本号恰好等于当时的 DHConfigVersion,
|
||||
// 此后再也不会刷新——版本号是纯常量,服务端自己永远不会主动使旧缓存过期)。
|
||||
// 本次从 1→2 是为诊断一例「A 拨 B 接通即断(key fingerprint/Ga hash 不合)」而提升,
|
||||
// 与本次通话 bug 排查同批次的服务端改动一起看。
|
||||
const DHConfigVersion = 2
|
||||
// 早先从 1→2 是为诊断一例「A 拨 B 接通即断(key fingerprint/Ga hash 不合)」而提升。
|
||||
// 上游又发现了同类问题的另一诱因:私有 DC 客户端若带着同为某个旧 version、但来自
|
||||
// 另一配置 profile 的缓存,服务端错误返回 NotModified 会让密聊两端用不同 p/g;
|
||||
// 这里直接取上游更新、更高的版本号,一次性使两类陈旧缓存都失效。
|
||||
const DHConfigVersion = 20260811
|
||||
|
||||
// DHG 是 DH generator。与官方一致取 3:TDesktop MTP::IsPrimeAndGood 对
|
||||
// 「官方 2048-bit prime + g∈{3,4,5,7}」有白名单快速通过路径,DrKLO native 同。
|
||||
|
|
|
|||
|
|
@ -487,43 +487,7 @@ func dedupNonZero(ids []int64) []int64 {
|
|||
}
|
||||
|
||||
func Evaluate(rules domain.PrivacyRules, ctx domain.PrivacyContext) bool {
|
||||
if ctx.OwnerUserID != 0 && ctx.OwnerUserID == ctx.ViewerUserID {
|
||||
return true
|
||||
}
|
||||
if len(rules.Rules) == 0 {
|
||||
rules.Rules = domain.DefaultPrivacyRules(rules.Key)
|
||||
}
|
||||
for _, rule := range rules.Rules {
|
||||
if explicitDisallowMatches(rule, ctx) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.Rules {
|
||||
if explicitAllowMatches(rule, ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.Rules {
|
||||
switch rule.Kind {
|
||||
case domain.PrivacyRuleDisallowContacts:
|
||||
if ctx.ViewerIsContact {
|
||||
return false
|
||||
}
|
||||
case domain.PrivacyRuleAllowContacts:
|
||||
if ctx.ViewerIsContact {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.Rules {
|
||||
switch rule.Kind {
|
||||
case domain.PrivacyRuleDisallowAll:
|
||||
return false
|
||||
case domain.PrivacyRuleAllowAll:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return domain.EvaluatePrivacy(rules, ctx)
|
||||
}
|
||||
|
||||
func ValidKey(key domain.PrivacyKey) bool {
|
||||
|
|
@ -588,52 +552,6 @@ func validateRules(rules []domain.PrivacyRule) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func explicitDisallowMatches(rule domain.PrivacyRule, ctx domain.PrivacyContext) bool {
|
||||
switch rule.Kind {
|
||||
case domain.PrivacyRuleDisallowUsers:
|
||||
return slices.Contains(rule.UserIDs, ctx.ViewerUserID)
|
||||
case domain.PrivacyRuleDisallowChatParticipants:
|
||||
return intersects(rule.ChatIDs, ctx.SharedChatIDs)
|
||||
case domain.PrivacyRuleDisallowBots:
|
||||
return ctx.ViewerIsBot
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func explicitAllowMatches(rule domain.PrivacyRule, ctx domain.PrivacyContext) bool {
|
||||
switch rule.Kind {
|
||||
case domain.PrivacyRuleAllowUsers:
|
||||
return slices.Contains(rule.UserIDs, ctx.ViewerUserID)
|
||||
case domain.PrivacyRuleAllowChatParticipants:
|
||||
return intersects(rule.ChatIDs, ctx.SharedChatIDs)
|
||||
case domain.PrivacyRuleAllowCloseFriends:
|
||||
return ctx.ViewerCloseFriend
|
||||
case domain.PrivacyRuleAllowPremium:
|
||||
return ctx.ViewerIsPremium
|
||||
case domain.PrivacyRuleAllowBots:
|
||||
return ctx.ViewerIsBot
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func intersects(a, b []int64) bool {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
set := make(map[int64]struct{}, len(a))
|
||||
for _, id := range a {
|
||||
set[id] = struct{}{}
|
||||
}
|
||||
for _, id := range b {
|
||||
if _, ok := set[id]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func defaultRules(ownerUserID int64, key domain.PrivacyKey) domain.PrivacyRules {
|
||||
return domain.PrivacyRules{
|
||||
OwnerUserID: ownerUserID,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,12 @@ func (s *Service) PrivacyBaseUsers(ctx context.Context, userIDs []int64) ([]doma
|
|||
return s.loadBaseUsersByIDs(ctx, userIDs)
|
||||
}
|
||||
|
||||
// BaseUsersByIDs returns viewer-independent identities for bounded write/read
|
||||
// validation paths without constructing full viewer projections.
|
||||
func (s *Service) BaseUsersByIDs(ctx context.Context, userIDs []int64) ([]domain.User, error) {
|
||||
return s.loadBaseUsersByIDs(ctx, userIDs)
|
||||
}
|
||||
|
||||
// ByIDs 批量返回指定用户。调用方必须已登录;缺失用户不会出现在结果中。
|
||||
func (s *Service) ByIDs(ctx context.Context, currentUserID int64, userIDs []int64) ([]domain.User, error) {
|
||||
if currentUserID == 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue