Ctrl K
ComunidadGuías…

media.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}