package files import ( "context" "fmt" "time" "go.uber.org/zap" "telesrv/internal/domain" ) // maxBotAvatarSourceBytes bounds the source image @BotFather turns into a bot's // profile photo. A profile photo a user sends in a chat is a server-rendered // message-size JPEG, comfortably under this. const maxBotAvatarSourceBytes = 12 << 20 // SetAvatarFromExistingPhoto renders an already-stored photo (typically one a // user just sent to a service bot) into the s/a/c avatar size set and makes it // the current profile photo of ownerType/ownerID. // // It is the path @BotFather's "Edit Botpic" uses: photos.uploadProfilePhoto#bot // is not accepted, and the source is a message photo rather than a fresh upload, // so the ordinary UploadProfilePhoto path does not apply. func (s *Service) SetAvatarFromExistingPhoto(ctx context.Context, ownerType domain.PeerType, ownerID, sourcePhotoID int64, date int) (domain.Photo, error) { if ownerID <= 0 || sourcePhotoID <= 0 { return domain.Photo{}, domain.ErrPhotoInvalid } source, found, err := s.media.GetPhoto(ctx, sourcePhotoID) if err != nil { return domain.Photo{}, err } if !found { return domain.Photo{}, domain.ErrPhotoInvalid } data, ok := s.photoSourceBytes(ctx, source) if !ok || !s.ValidateAvatarUpload(data) { return domain.Photo{}, domain.ErrPhotoInvalid } if date == 0 { date = int(time.Now().Unix()) } photo, err := s.createAvatarPhoto(ctx, data, ownerID) if err != nil { return domain.Photo{}, err } if err := s.media.AddProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photo.ID, date); err != nil { return domain.Photo{}, err } return photo, nil } // PeerHasAvatar reports whether ownerType/ownerID has a current profile photo. func (s *Service) PeerHasAvatar(ctx context.Context, ownerType domain.PeerType, ownerID int64) (bool, error) { _, ok, err := s.CurrentProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile) return ok, err } // photoSourceBytes reads the original image bytes behind a stored photo. Every // static size of a photo written by putPhotoStaticSizes points at the same // stored object, so any one size yields the full image. func (s *Service) photoSourceBytes(ctx context.Context, photo domain.Photo) ([]byte, bool) { for _, size := range photo.Sizes { if size.Type == "" { continue } key := fmt.Sprintf("photo:%d:%s", photo.ID, size.Type) blob, found, err := s.media.GetFileBlob(ctx, key) if err != nil || !found || blob.Size <= 0 || blob.Size > maxBotAvatarSourceBytes { continue } backend, err := s.backendFor(blob.Backend) if err != nil { continue } body, total, err := backend.GetRange(ctx, blob.ObjectKey, 0, blob.Size) if err != nil || total != blob.Size || int64(len(body)) != blob.Size { s.log.Warn("read bot avatar source blob failed", zap.String("location_key", key), zap.Error(err)) continue } return body, true } return nil, false }