media.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"cmp"
"errors"
"fmt"
"image"
"io"
"net/http"
"path/filepath"
"strings"
"unicode/utf16"
"github.com/go-telegram/bot/models"
"github.com/max-messenger/max-bot-api-client-go/v2/model"
)
// media is an attachment of a Telegram post and what it becomes in MAX.
type media struct {
fileID string
size int64
name string
upload model.UploadType
attach model.AttachmentType
width, height, duration int
}
func mediaOf(m *models.Message) (media, bool) {
image := func(sizes []models.PhotoSize) media {
p := sizes[len(sizes)-1] // the largest size
return media{fileID: p.FileID, size: int64(p.FileSize), name: "photo.jpg", upload: model.UploadImage, attach: model.AttachImage, width: p.Width, height: p.Height}
}
video := func(fileID string, size int64, name, fallback string) media {
return media{fileID: fileID, size: size, name: cmp.Or(name, fallback), upload: model.UploadVideo, attach: model.AttachVideo}
}
file := func(fileID string, size int64, name, fallback string) media {
return media{fileID: fileID, size: size, name: cmp.Or(name, fallback), upload: model.UploadFile, attach: model.AttachFile}
}
switch {
case len(m.Photo) > 0:
return image(m.Photo), true
case m.LivePhoto != nil && len(m.LivePhoto.Photo) > 0:
return image(m.LivePhoto.Photo), true
case m.Video != nil:
return video(m.Video.FileID, m.Video.FileSize, m.Video.FileName, "video.mp4"), true
// Before Document: Telegram fills both fields for a GIF
case m.Animation != nil:
a := m.Animation
md := video(a.FileID, a.FileSize, a.FileName, "animation.mp4")
md.width, md.height = a.Width, a.Height
if a.MimeType == "image/gif" || (a.MimeType == "" && strings.EqualFold(filepath.Ext(a.FileName), ".gif")) {
md.upload, md.attach = model.UploadImage, model.AttachImage
md.name = cmp.Or(a.FileName, "animation.gif")
}
return md, true
case m.VideoNote != nil:
return video(m.VideoNote.FileID, int64(m.VideoNote.FileSize), "", "video_note.mp4"), true
case m.Voice != nil:
return media{fileID: m.Voice.FileID, size: m.Voice.FileSize, name: "voice.ogg", upload: model.UploadAudio, attach: model.AttachAudio, duration: m.Voice.Duration}, true
case m.Audio != nil:
return file(m.Audio.FileID, m.Audio.FileSize, m.Audio.FileName, "audio.mp3"), true
case m.Document != nil:
return file(m.Document.FileID, m.Document.FileSize, m.Document.FileName, "file"), true
}
return media{}, false
}
var errMaxMediaInvalid = errors.New("вложение не соответствует ограничениям MAX")
func (m media) validate(size int64) error {
limit := int64(4_000_000_000)
switch m.upload {
case model.UploadImage:
limit = 50_000_000
if m.width > 7680 || m.height > 7680 {
return fmt.Errorf("%w: изображение больше 7680 × 7680", errMaxMediaInvalid)
}
case model.UploadVideo:
limit = 250_000_000
case model.UploadAudio:
limit = 256_000_000
if m.duration > 3600 {
return fmt.Errorf("%w: аудио длиннее 60 минут", errMaxMediaInvalid)
}
}
if size > limit {
return fmt.Errorf("%w: %s больше %d МБ", errMaxMediaInvalid, m.upload, limit/1_000_000)
}
return nil
}
// Telegram can return GIF animation bytes even when the name ends in .mp4.
func (m media) prepared(file tgFile) (media, error) {
if m.upload == model.UploadVideo || m.upload == model.UploadImage {
r, err := file.open()
if err != nil {
return m, err
}
defer func() { _ = r.Close() }()
var head [512]byte
n, readErr := io.ReadFull(r, head[:])
if readErr != nil && !errors.Is(readErr, io.EOF) && !errors.Is(readErr, io.ErrUnexpectedEOF) {
return m, readErr
}
if n >= 6 && (string(head[:6]) == "GIF87a" || string(head[:6]) == "GIF89a") {
m.upload, m.attach = model.UploadImage, model.AttachImage
m.name = strings.TrimSuffix(m.name, filepath.Ext(m.name)) + ".gif"
} else if http.DetectContentType(head[:n]) == "video/mp4" && m.upload == model.UploadImage {
m.upload, m.attach = model.UploadVideo, model.AttachVideo
m.name = strings.TrimSuffix(m.name, filepath.Ext(m.name)) + ".mp4"
}
}
if m.upload == model.UploadImage {
r, err := file.open()
if err != nil {
return m, err
}
cfg, _, decodeErr := image.DecodeConfig(r)
_ = r.Close()
if decodeErr == nil {
m.width, m.height = cfg.Width, cfg.Height
}
}
return m, m.validate(file.size)
}
// unsupported is content not mirrored by perepost. Some types exist on MAX
// too, but need a separate conversion. Stickers and service messages are skipped.
func unsupported(m *models.Message) bool {
return m.Poll != nil || m.Location != nil || m.Contact != nil || m.PaidMedia != nil ||
m.Story != nil || m.Game != nil || m.Checklist != nil || m.Giveaway != nil ||
m.Invoice != nil || m.RichMessage != nil
}
// forwardedFrom is the "Переслано из …" line of a repost: without it someone
// else's post would look like the channel's own in MAX.
func forwardedFrom(m *models.Message) (richText, bool) {
o := m.ForwardOrigin
if o == nil {
return richText{}, false
}
var prefix, name, link string
switch {
case o.MessageOriginChannel != nil:
c := o.MessageOriginChannel
prefix, name = "Переслано из ", c.Chat.Title
if c.Chat.Username != "" {
link = fmt.Sprintf("https://t.me/%s/%d", c.Chat.Username, c.MessageID)
}
case o.MessageOriginChat != nil:
c := o.MessageOriginChat.SenderChat
prefix, name = "Переслано из ", c.Title
if c.Username != "" {
link = "https://t.me/" + c.Username
}
case o.MessageOriginUser != nil:
u := o.MessageOriginUser.SenderUser
prefix, name = "Переслано от ", strings.TrimSpace(u.FirstName+" "+u.LastName)
if u.Username != "" {
link = "https://t.me/" + u.Username
}
case o.MessageOriginHiddenUser != nil:
prefix, name = "Переслано от ", o.MessageOriginHiddenUser.SenderUserName
}
if name == "" {
return richText{}, false
}
t := newRichText(prefix+name, nil)
t.entities = append(t.entities, models.MessageEntity{Type: models.MessageEntityTypeItalic, Length: len(t.units)})
if link != "" {
offset := len(utf16.Encode([]rune(prefix)))
t.entities = append(t.entities, models.MessageEntity{
Type: models.MessageEntityTypeTextLink, Offset: offset, Length: len(t.units) - offset, URL: link,
})
}
return t, true
}