- Edit Bot now shows the current value of every field (Name/About/ Description/Botpic/Commands) like BotFather, with real botpic status via a new PeerHasAvatar port method. - After editing a field the dialog lands back on a fresh Edit Bot menu (working "Back to bot" / "Bots list" buttons) instead of ending, so a follow-up button press no longer reports the button as expired. - Service-bot messages now render @username as a tappable mention entity.
84 lines
2.9 KiB
Go
84 lines
2.9 KiB
Go
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
|
|
}
|