Merge pull request #469 from go-telegram-bot-api/files

Create interface for file data
bot-api-6.1
Syfaro 2021-11-08 14:26:17 -05:00 committed by GitHub
commit 7c82078b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 283 additions and 241 deletions

59
bot.go
View File

@ -3,7 +3,6 @@
package tgbotapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@ -12,7 +11,6 @@ import (
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"
"time"
)
@ -185,54 +183,37 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi
}
for _, file := range files {
switch f := file.File.(type) {
case string:
fileHandle, err := os.Open(f)
if err != nil {
w.CloseWithError(err)
return
}
defer fileHandle.Close()
part, err := m.CreateFormFile(file.Name, fileHandle.Name())
if file.Data.NeedsUpload() {
name, reader, err := file.Data.UploadData()
if err != nil {
w.CloseWithError(err)
return
}
io.Copy(part, fileHandle)
case FileBytes:
part, err := m.CreateFormFile(file.Name, f.Name)
part, err := m.CreateFormFile(file.Name, name)
if err != nil {
w.CloseWithError(err)
return
}
buf := bytes.NewBuffer(f.Bytes)
io.Copy(part, buf)
case FileReader:
part, err := m.CreateFormFile(file.Name, f.Name)
if err != nil {
if _, err := io.Copy(part, reader); err != nil {
w.CloseWithError(err)
return
}
io.Copy(part, f.Reader)
case FileURL:
val := string(f)
if err := m.WriteField(file.Name, val); err != nil {
if closer, ok := reader.(io.ReadCloser); ok {
if err = closer.Close(); err != nil {
w.CloseWithError(err)
return
}
case FileID:
val := string(f)
if err := m.WriteField(file.Name, val); err != nil {
}
} else {
value := file.Data.SendData()
if err := m.WriteField(file.Name, value); err != nil {
w.CloseWithError(err)
return
}
default:
w.CloseWithError(errors.New(ErrBadFileType))
return
}
}
}()
@ -321,8 +302,7 @@ func (bot *BotAPI) IsMessageToMe(message Message) bool {
func hasFilesNeedingUpload(files []RequestFile) bool {
for _, file := range files {
switch file.File.(type) {
case string, FileBytes, FileReader:
if file.Data.NeedsUpload() {
return true
}
}
@ -349,20 +329,7 @@ func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
// However, if there are no files to be uploaded, there's likely things
// that need to be turned into params instead.
for _, file := range files {
var s string
switch f := file.File.(type) {
case string:
s = f
case FileID:
s = string(f)
case FileURL:
s = string(f)
default:
return nil, errors.New(ErrBadFileType)
}
params[file.Name] = s
params[file.Name] = file.Data.SendData()
}
}

View File

@ -127,7 +127,7 @@ func TestCopyMessage(t *testing.T) {
func TestSendWithNewPhoto(t *testing.T) {
bot, _ := getBot(t)
msg := NewPhoto(ChatID, "tests/image.jpg")
msg := NewPhoto(ChatID, FilePath("tests/image.jpg"))
msg.Caption = "Test"
_, err := bot.Send(msg)
@ -169,7 +169,7 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) {
func TestSendWithNewPhotoReply(t *testing.T) {
bot, _ := getBot(t)
msg := NewPhoto(ChatID, "tests/image.jpg")
msg := NewPhoto(ChatID, FilePath("tests/image.jpg"))
msg.ReplyToMessageID = ReplyToMessageID
_, err := bot.Send(msg)
@ -182,7 +182,7 @@ func TestSendWithNewPhotoReply(t *testing.T) {
func TestSendNewPhotoToChannel(t *testing.T) {
bot, _ := getBot(t)
msg := NewPhotoToChannel(Channel, "tests/image.jpg")
msg := NewPhotoToChannel(Channel, FilePath("tests/image.jpg"))
msg.Caption = "Test"
_, err := bot.Send(msg)
@ -239,7 +239,7 @@ func TestSendWithExistingPhoto(t *testing.T) {
func TestSendWithNewDocument(t *testing.T) {
bot, _ := getBot(t)
msg := NewDocument(ChatID, "tests/image.jpg")
msg := NewDocument(ChatID, FilePath("tests/image.jpg"))
_, err := bot.Send(msg)
if err != nil {
@ -250,8 +250,8 @@ func TestSendWithNewDocument(t *testing.T) {
func TestSendWithNewDocumentAndThumb(t *testing.T) {
bot, _ := getBot(t)
msg := NewDocument(ChatID, "tests/voice.ogg")
msg.Thumb = "tests/image.jpg"
msg := NewDocument(ChatID, FilePath("tests/voice.ogg"))
msg.Thumb = FilePath("tests/image.jpg")
_, err := bot.Send(msg)
if err != nil {
@ -273,7 +273,7 @@ func TestSendWithExistingDocument(t *testing.T) {
func TestSendWithNewAudio(t *testing.T) {
bot, _ := getBot(t)
msg := NewAudio(ChatID, "tests/audio.mp3")
msg := NewAudio(ChatID, FilePath("tests/audio.mp3"))
msg.Title = "TEST"
msg.Duration = 10
msg.Performer = "TEST"
@ -302,7 +302,7 @@ func TestSendWithExistingAudio(t *testing.T) {
func TestSendWithNewVoice(t *testing.T) {
bot, _ := getBot(t)
msg := NewVoice(ChatID, "tests/voice.ogg")
msg := NewVoice(ChatID, FilePath("tests/voice.ogg"))
msg.Duration = 10
_, err := bot.Send(msg)
@ -356,7 +356,7 @@ func TestSendWithVenue(t *testing.T) {
func TestSendWithNewVideo(t *testing.T) {
bot, _ := getBot(t)
msg := NewVideo(ChatID, "tests/video.mp4")
msg := NewVideo(ChatID, FilePath("tests/video.mp4"))
msg.Duration = 10
msg.Caption = "TEST"
@ -384,7 +384,7 @@ func TestSendWithExistingVideo(t *testing.T) {
func TestSendWithNewVideoNote(t *testing.T) {
bot, _ := getBot(t)
msg := NewVideoNote(ChatID, 240, "tests/videonote.mp4")
msg := NewVideoNote(ChatID, 240, FilePath("tests/videonote.mp4"))
msg.Duration = 10
_, err := bot.Send(msg)
@ -410,7 +410,7 @@ func TestSendWithExistingVideoNote(t *testing.T) {
func TestSendWithNewSticker(t *testing.T) {
bot, _ := getBot(t)
msg := NewSticker(ChatID, "tests/image.jpg")
msg := NewSticker(ChatID, FilePath("tests/image.jpg"))
_, err := bot.Send(msg)
@ -434,7 +434,7 @@ func TestSendWithExistingSticker(t *testing.T) {
func TestSendWithNewStickerAndKeyboardHide(t *testing.T) {
bot, _ := getBot(t)
msg := NewSticker(ChatID, "tests/image.jpg")
msg := NewSticker(ChatID, FilePath("tests/image.jpg"))
msg.ReplyMarkup = ReplyKeyboardRemove{
RemoveKeyboard: true,
Selective: false,
@ -550,7 +550,7 @@ func TestSetWebhookWithCert(t *testing.T) {
bot.Request(DeleteWebhookConfig{})
wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem")
wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, FilePath("tests/cert.pem"))
if err != nil {
t.Error(err)
@ -609,8 +609,8 @@ func TestSendWithMediaGroupPhotoVideo(t *testing.T) {
cfg := NewMediaGroup(ChatID, []interface{}{
NewInputMediaPhoto(FileURL("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg")),
NewInputMediaPhoto("tests/image.jpg"),
NewInputMediaVideo("tests/video.mp4"),
NewInputMediaPhoto(FilePath("tests/image.jpg")),
NewInputMediaVideo(FilePath("tests/video.mp4")),
})
messages, err := bot.SendMediaGroup(cfg)
@ -632,7 +632,7 @@ func TestSendWithMediaGroupDocument(t *testing.T) {
cfg := NewMediaGroup(ChatID, []interface{}{
NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")),
NewInputMediaDocument("tests/image.jpg"),
NewInputMediaDocument(FilePath("tests/image.jpg")),
})
messages, err := bot.SendMediaGroup(cfg)
@ -653,8 +653,8 @@ func TestSendWithMediaGroupAudio(t *testing.T) {
bot, _ := getBot(t)
cfg := NewMediaGroup(ChatID, []interface{}{
NewInputMediaAudio("tests/audio.mp3"),
NewInputMediaAudio("tests/audio.mp3"),
NewInputMediaAudio(FilePath("tests/audio.mp3")),
NewInputMediaAudio(FilePath("tests/audio.mp3")),
})
messages, err := bot.SendMediaGroup(cfg)
@ -715,7 +715,7 @@ func ExampleNewWebhook() {
log.Printf("Authorized on account %s", bot.Self.UserName)
wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")
wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem"))
if err != nil {
panic(err)
@ -755,7 +755,7 @@ func ExampleWebhookHandler() {
log.Printf("Authorized on account %s", bot.Self.UserName)
wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")
wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem"))
if err != nil {
panic(err)
@ -1019,7 +1019,7 @@ func TestCommands(t *testing.T) {
// ChatID: ChatID,
// MessageID: m.MessageID,
// },
// Media: NewInputMediaVideo("tests/video.mp4"),
// Media: NewInputMediaVideo(FilePath("tests/video.mp4")),
// }
// _, err = bot.Request(edit)
@ -1030,17 +1030,17 @@ func TestCommands(t *testing.T) {
func TestPrepareInputMediaForParams(t *testing.T) {
media := []interface{}{
NewInputMediaPhoto("tests/image.jpg"),
NewInputMediaPhoto(FilePath("tests/image.jpg")),
NewInputMediaVideo(FileID("test")),
}
prepared := prepareInputMediaForParams(media)
if media[0].(InputMediaPhoto).Media != "tests/image.jpg" {
if media[0].(InputMediaPhoto).Media != FilePath("tests/image.jpg") {
t.Error("Original media was changed")
}
if prepared[0].(InputMediaPhoto).Media != "attach://file-0" {
if prepared[0].(InputMediaPhoto).Media != fileAttach("attach://file-0") {
t.Error("New media was not replaced")
}

View File

@ -1,9 +1,11 @@
package tgbotapi
import (
"bytes"
"fmt"
"io"
"net/url"
"os"
"strconv"
)
@ -98,8 +100,6 @@ const (
// Library errors
const (
// ErrBadFileType happens when you pass an unknown type
ErrBadFileType = "bad file type"
ErrBadURL = "bad or empty url"
)
@ -109,21 +109,136 @@ type Chattable interface {
method() string
}
// RequestFile represents a file associated with a request. May involve
// uploading a file, or passing an existing ID.
type RequestFile struct {
// The multipart upload field name.
Name string
// The file to upload.
File interface{}
}
// Fileable is any config type that can be sent that includes a file.
type Fileable interface {
Chattable
files() []RequestFile
}
// RequestFile represents a file associated with a field name.
type RequestFile struct {
// The file field name.
Name string
// The file data to include.
Data RequestFileData
}
// RequestFileData represents the data to be used for a file.
type RequestFileData interface {
// If the file needs to be uploaded.
NeedsUpload() bool
// Get the file name and an `io.Reader` for the file to be uploaded. This
// must only be called when the file needs to be uploaded.
UploadData() (string, io.Reader, error)
// Get the file data to send when a file does not need to be uploaded. This
// must only be called when the file does not need to be uploaded.
SendData() string
}
// FileBytes contains information about a set of bytes to upload
// as a File.
type FileBytes struct {
Name string
Bytes []byte
}
func (fb FileBytes) NeedsUpload() bool {
return true
}
func (fb FileBytes) UploadData() (string, io.Reader, error) {
return fb.Name, bytes.NewReader(fb.Bytes), nil
}
func (fb FileBytes) SendData() string {
panic("FileBytes must be uploaded")
}
// FileReader contains information about a reader to upload as a File.
type FileReader struct {
Name string
Reader io.Reader
}
func (fr FileReader) NeedsUpload() bool {
return true
}
func (fr FileReader) UploadData() (string, io.Reader, error) {
return fr.Name, fr.Reader, nil
}
func (fr FileReader) SendData() string {
panic("FileReader must be uploaded")
}
// FilePath is a path to a local file.
type FilePath string
func (fp FilePath) NeedsUpload() bool {
return true
}
func (fp FilePath) UploadData() (string, io.Reader, error) {
fileHandle, err := os.Open(string(fp))
if err != nil {
return "", nil, err
}
name := fileHandle.Name()
return name, fileHandle, err
}
func (fp FilePath) SendData() string {
panic("FilePath must be uploaded")
}
// FileURL is a URL to use as a file for a request.
type FileURL string
func (fu FileURL) NeedsUpload() bool {
return false
}
func (fu FileURL) UploadData() (string, io.Reader, error) {
panic("FileURL cannot be uploaded")
}
func (fu FileURL) SendData() string {
return string(fu)
}
// FileID is an ID of a file already uploaded to Telegram.
type FileID string
func (fi FileID) NeedsUpload() bool {
return false
}
func (fi FileID) UploadData() (string, io.Reader, error) {
panic("FileID cannot be uploaded")
}
func (fi FileID) SendData() string {
return string(fi)
}
// fileAttach is a internal file type used for processed media groups.
type fileAttach string
func (fa fileAttach) NeedsUpload() bool {
return false
}
func (fa fileAttach) UploadData() (string, io.Reader, error) {
panic("fileAttach cannot be uploaded")
}
func (fa fileAttach) SendData() string {
return string(fa)
}
// LogOutConfig is a request to log out of the cloud Bot API server.
//
// Note that you may not log back in for at least 10 minutes.
@ -177,7 +292,7 @@ func (chat *BaseChat) params() (Params, error) {
// BaseFile is a base type for all file config types.
type BaseFile struct {
BaseChat
File interface{}
File RequestFileData
}
func (file BaseFile) params() (Params, error) {
@ -292,7 +407,7 @@ func (config CopyMessageConfig) method() string {
// PhotoConfig contains information about a SendPhoto request.
type PhotoConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Caption string
ParseMode string
CaptionEntities []MessageEntity
@ -318,13 +433,13 @@ func (config PhotoConfig) method() string {
func (config PhotoConfig) files() []RequestFile {
files := []RequestFile{{
Name: "photo",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -334,7 +449,7 @@ func (config PhotoConfig) files() []RequestFile {
// AudioConfig contains information about a SendAudio request.
type AudioConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Caption string
ParseMode string
CaptionEntities []MessageEntity
@ -366,13 +481,13 @@ func (config AudioConfig) method() string {
func (config AudioConfig) files() []RequestFile {
files := []RequestFile{{
Name: "audio",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -382,7 +497,7 @@ func (config AudioConfig) files() []RequestFile {
// DocumentConfig contains information about a SendDocument request.
type DocumentConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Caption string
ParseMode string
CaptionEntities []MessageEntity
@ -406,13 +521,13 @@ func (config DocumentConfig) method() string {
func (config DocumentConfig) files() []RequestFile {
files := []RequestFile{{
Name: "document",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -435,14 +550,14 @@ func (config StickerConfig) method() string {
func (config StickerConfig) files() []RequestFile {
return []RequestFile{{
Name: "sticker",
File: config.File,
Data: config.File,
}}
}
// VideoConfig contains information about a SendVideo request.
type VideoConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Duration int
Caption string
ParseMode string
@ -472,13 +587,13 @@ func (config VideoConfig) method() string {
func (config VideoConfig) files() []RequestFile {
files := []RequestFile{{
Name: "video",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -489,7 +604,7 @@ func (config VideoConfig) files() []RequestFile {
type AnimationConfig struct {
BaseFile
Duration int
Thumb interface{}
Thumb RequestFileData
Caption string
ParseMode string
CaptionEntities []MessageEntity
@ -516,13 +631,13 @@ func (config AnimationConfig) method() string {
func (config AnimationConfig) files() []RequestFile {
files := []RequestFile{{
Name: "animation",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -532,7 +647,7 @@ func (config AnimationConfig) files() []RequestFile {
// VideoNoteConfig contains information about a SendVideoNote request.
type VideoNoteConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Duration int
Length int
}
@ -553,13 +668,13 @@ func (config VideoNoteConfig) method() string {
func (config VideoNoteConfig) files() []RequestFile {
files := []RequestFile{{
Name: "video_note",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -569,7 +684,7 @@ func (config VideoNoteConfig) files() []RequestFile {
// VoiceConfig contains information about a SendVoice request.
type VoiceConfig struct {
BaseFile
Thumb interface{}
Thumb RequestFileData
Caption string
ParseMode string
CaptionEntities []MessageEntity
@ -597,13 +712,13 @@ func (config VoiceConfig) method() string {
func (config VoiceConfig) files() []RequestFile {
files := []RequestFile{{
Name: "voice",
File: config.File,
Data: config.File,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -1046,7 +1161,7 @@ func (config UpdateConfig) params() (Params, error) {
// WebhookConfig contains information about a SetWebhook request.
type WebhookConfig struct {
URL *url.URL
Certificate interface{}
Certificate RequestFileData
IPAddress string
MaxConnections int
AllowedUpdates []string
@ -1076,7 +1191,7 @@ func (config WebhookConfig) files() []RequestFile {
if config.Certificate != nil {
return []RequestFile{{
Name: "certificate",
File: config.Certificate,
Data: config.Certificate,
}}
}
@ -1100,25 +1215,6 @@ func (config DeleteWebhookConfig) params() (Params, error) {
return params, nil
}
// FileBytes contains information about a set of bytes to upload
// as a File.
type FileBytes struct {
Name string
Bytes []byte
}
// FileReader contains information about a reader to upload as a File.
type FileReader struct {
Name string
Reader io.Reader
}
// FileURL is a URL to use as a file for a request.
type FileURL string
// FileID is an ID of a file already uploaded to Telegram.
type FileID string
// InlineConfig contains information on making an InlineQuery response.
type InlineConfig struct {
InlineQueryID string `json:"inline_query_id"`
@ -1753,7 +1849,7 @@ func (config SetChatPhotoConfig) method() string {
func (config SetChatPhotoConfig) files() []RequestFile {
return []RequestFile{{
Name: "photo",
File: config.File,
Data: config.File,
}}
}
@ -1837,7 +1933,7 @@ func (config GetStickerSetConfig) params() (Params, error) {
// UploadStickerConfig allows you to upload a sticker for use in a set later.
type UploadStickerConfig struct {
UserID int64
PNGSticker interface{}
PNGSticker RequestFileData
}
func (config UploadStickerConfig) method() string {
@ -1855,7 +1951,7 @@ func (config UploadStickerConfig) params() (Params, error) {
func (config UploadStickerConfig) files() []RequestFile {
return []RequestFile{{
Name: "png_sticker",
File: config.PNGSticker,
Data: config.PNGSticker,
}}
}
@ -1866,8 +1962,8 @@ type NewStickerSetConfig struct {
UserID int64
Name string
Title string
PNGSticker interface{}
TGSSticker interface{}
PNGSticker RequestFileData
TGSSticker RequestFileData
Emojis string
ContainsMasks bool
MaskPosition *MaskPosition
@ -1897,13 +1993,13 @@ func (config NewStickerSetConfig) files() []RequestFile {
if config.PNGSticker != nil {
return []RequestFile{{
Name: "png_sticker",
File: config.PNGSticker,
Data: config.PNGSticker,
}}
}
return []RequestFile{{
Name: "tgs_sticker",
File: config.TGSSticker,
Data: config.TGSSticker,
}}
}
@ -1911,8 +2007,8 @@ func (config NewStickerSetConfig) files() []RequestFile {
type AddStickerConfig struct {
UserID int64
Name string
PNGSticker interface{}
TGSSticker interface{}
PNGSticker RequestFileData
TGSSticker RequestFileData
Emojis string
MaskPosition *MaskPosition
}
@ -1937,13 +2033,13 @@ func (config AddStickerConfig) files() []RequestFile {
if config.PNGSticker != nil {
return []RequestFile{{
Name: "png_sticker",
File: config.PNGSticker,
Data: config.PNGSticker,
}}
}
return []RequestFile{{
Name: "tgs_sticker",
File: config.TGSSticker,
Data: config.TGSSticker,
}}
}
@ -1988,7 +2084,7 @@ func (config DeleteStickerConfig) params() (Params, error) {
type SetStickerSetThumbConfig struct {
Name string
UserID int64
Thumb interface{}
Thumb RequestFileData
}
func (config SetStickerSetThumbConfig) method() string {
@ -2007,7 +2103,7 @@ func (config SetStickerSetThumbConfig) params() (Params, error) {
func (config SetStickerSetThumbConfig) files() []RequestFile {
return []RequestFile{{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
}}
}
@ -2181,45 +2277,38 @@ func (config DeleteMyCommandsConfig) params() (Params, error) {
func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} {
switch m := inputMedia.(type) {
case InputMediaPhoto:
switch m.Media.(type) {
case string, FileBytes, FileReader:
m.Media = fmt.Sprintf("attach://file-%d", idx)
if m.Media.NeedsUpload() {
m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx))
}
return m
case InputMediaVideo:
switch m.Media.(type) {
case string, FileBytes, FileReader:
m.Media = fmt.Sprintf("attach://file-%d", idx)
if m.Media.NeedsUpload() {
m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx))
}
switch m.Thumb.(type) {
case string, FileBytes, FileReader:
m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx)
if m.Thumb != nil && m.Thumb.NeedsUpload() {
m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx))
}
return m
case InputMediaAudio:
switch m.Media.(type) {
case string, FileBytes, FileReader:
m.Media = fmt.Sprintf("attach://file-%d", idx)
if m.Media.NeedsUpload() {
m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx))
}
switch m.Thumb.(type) {
case string, FileBytes, FileReader:
m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx)
if m.Thumb != nil && m.Thumb.NeedsUpload() {
m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx))
}
return m
case InputMediaDocument:
switch m.Media.(type) {
case string, FileBytes, FileReader:
m.Media = fmt.Sprintf("attach://file-%d", idx)
if m.Media.NeedsUpload() {
m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx))
}
switch m.Thumb.(type) {
case string, FileBytes, FileReader:
m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx)
if m.Thumb != nil && m.Thumb.NeedsUpload() {
m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx))
}
return m
@ -2241,59 +2330,52 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile {
switch m := inputMedia.(type) {
case InputMediaPhoto:
switch f := m.Media.(type) {
case string, FileBytes, FileReader:
if m.Media.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Media,
})
}
case InputMediaVideo:
switch f := m.Media.(type) {
case string, FileBytes, FileReader:
if m.Media.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Media,
})
}
switch f := m.Thumb.(type) {
case string, FileBytes, FileReader:
if m.Thumb != nil && m.Thumb.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d-thumb", idx),
File: f,
Name: fmt.Sprintf("file-%d", idx),
Data: m.Thumb,
})
}
case InputMediaDocument:
switch f := m.Media.(type) {
case string, FileBytes, FileReader:
if m.Media.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Media,
})
}
switch f := m.Thumb.(type) {
case string, FileBytes, FileReader:
if m.Thumb != nil && m.Thumb.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Thumb,
})
}
case InputMediaAudio:
switch f := m.Media.(type) {
case string, FileBytes, FileReader:
if m.Media.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Media,
})
}
switch f := m.Thumb.(type) {
case string, FileBytes, FileReader:
if m.Thumb != nil && m.Thumb.NeedsUpload() {
files = append(files, RequestFile{
Name: fmt.Sprintf("file-%d", idx),
File: f,
Data: m.Thumb,
})
}
}

View File

@ -3,20 +3,22 @@
Telegram supports specifying files in many different formats. In order to
accommodate them all, there are multiple structs and type aliases required.
All of these types implement the `RequestFileData` interface.
| Type | Description |
| ------------ | ------------------------------------------------------------------------- |
| `string` | Used as a local path to a file |
| `FilePath` | A local path to a file |
| `FileID` | Existing file ID on Telegram's servers |
| `FileURL` | URL to file, must be served with expected MIME type |
| `FileReader` | Use an `io.Reader` to provide a file. Lazily read to save memory. |
| `FileBytes` | `[]byte` containing file data. Prefer to use `FileReader` to save memory. |
## `string`
## `FilePath`
A path to a local file.
```go
file := "tests/image.jpg"
file := tgbotapi.FilePath("tests/image.jpg")
```
## `FileID`

View File

@ -19,7 +19,8 @@ type DeleteMessageConfig struct {
}
```
What type should `ChatID` be? Telegram allows specifying numeric chat IDs or channel usernames. Golang doesn't have union types, and interfaces are entirely
What type should `ChatID` be? Telegram allows specifying numeric chat IDs or
channel usernames. Golang doesn't have union types, and interfaces are entirely
untyped. This library solves this by adding two fields, a `ChatID` and a
`ChannelUsername`. We can now write the struct as follows.
@ -103,8 +104,8 @@ have similar fields for their files.
ChannelUsername string
ChatID int64
MessageID int
+ Delete interface{}
+ Thumb interface{}
+ Delete RequestFileData
+ Thumb RequestFileData
}
```
@ -115,13 +116,13 @@ and add the `thumb` file if we have one.
func (config DeleteMessageConfig) files() []RequestFile {
files := []RequestFile{{
Name: "delete",
File: config.Delete,
Data: config.Delete,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -129,7 +130,8 @@ func (config DeleteMessageConfig) files() []RequestFile {
}
```
And now our files will upload! It will transparently handle uploads whether File is a string with a path to a file, `FileURL`, `FileBytes`, `FileReader`, or `FileID`.
And now our files will upload! It will transparently handle uploads whether File
is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`.
### Base Configs

View File

@ -28,14 +28,14 @@ func (config DocumentConfig) files() []RequestFile {
// there always is a document file, so initialize the array with that.
files := []RequestFile{{
Name: "document",
File: config.File,
Data: config.File,
}}
// We'll only add a file if we have one.
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
File: config.Thumb,
Data: config.Thumb,
})
}
@ -58,7 +58,7 @@ Let's follow through creating a new media group with string and file uploads.
First, we start by creating some `InputMediaPhoto`.
```go
photo := tgbotapi.NewInputMediaPhoto("tests/image.jpg")
photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg"))
```
@ -85,24 +85,3 @@ are all changed into `attach://file-%d`. When collecting a list of files to
upload, it names them the same way. This creates a nearly transparent way of
handling multiple files in the background without the user having to consider
what's going on.
## Library Processing
If at some point in the future new upload types are required, let's talk about
where the current types are used.
Upload types are defined in `configs.go`. Where possible, type aliases are
preferred. Structs can be used when multiple fields are required.
The main usage of the upload types happens in `UploadFiles`. It switches on each
file's type in order to determine how to upload it. Files that aren't uploaded
(file IDs, URLs) are converted back into strings and passed through as strings
into the correct field. Uploaded types are processed as needed (opening files,
etc.) and written into the form using a copy approach in a goroutine to reduce
memory usage.
In addition to `UploadFiles`, there's more processing of upload types in the
`prepareInputMediaParam` and `prepareInputMediaFile` functions. These look at
the `InputMedia` types to determine which files are uploaded and which are
passed through as strings. They only need to be aware of which files need to be
replaced with `attach://` fields.

View File

@ -70,7 +70,7 @@ func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageCo
// FileReader, or FileBytes.
//
// Note that you must send animated GIFs as a document.
func NewPhoto(chatID int64, file interface{}) PhotoConfig {
func NewPhoto(chatID int64, file RequestFileData) PhotoConfig {
return PhotoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -82,7 +82,7 @@ func NewPhoto(chatID int64, file interface{}) PhotoConfig {
// NewPhotoToChannel creates a new photo uploader to send a photo to a channel.
//
// Note that you must send animated GIFs as a document.
func NewPhotoToChannel(username string, file interface{}) PhotoConfig {
func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig {
return PhotoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{
@ -94,7 +94,7 @@ func NewPhotoToChannel(username string, file interface{}) PhotoConfig {
}
// NewAudio creates a new sendAudio request.
func NewAudio(chatID int64, file interface{}) AudioConfig {
func NewAudio(chatID int64, file RequestFileData) AudioConfig {
return AudioConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -104,7 +104,7 @@ func NewAudio(chatID int64, file interface{}) AudioConfig {
}
// NewDocument creates a new sendDocument request.
func NewDocument(chatID int64, file interface{}) DocumentConfig {
func NewDocument(chatID int64, file RequestFileData) DocumentConfig {
return DocumentConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -114,7 +114,7 @@ func NewDocument(chatID int64, file interface{}) DocumentConfig {
}
// NewSticker creates a new sendSticker request.
func NewSticker(chatID int64, file interface{}) StickerConfig {
func NewSticker(chatID int64, file RequestFileData) StickerConfig {
return StickerConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -124,7 +124,7 @@ func NewSticker(chatID int64, file interface{}) StickerConfig {
}
// NewVideo creates a new sendVideo request.
func NewVideo(chatID int64, file interface{}) VideoConfig {
func NewVideo(chatID int64, file RequestFileData) VideoConfig {
return VideoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -134,7 +134,7 @@ func NewVideo(chatID int64, file interface{}) VideoConfig {
}
// NewAnimation creates a new sendAnimation request.
func NewAnimation(chatID int64, file interface{}) AnimationConfig {
func NewAnimation(chatID int64, file RequestFileData) AnimationConfig {
return AnimationConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -147,7 +147,7 @@ func NewAnimation(chatID int64, file interface{}) AnimationConfig {
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig {
func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig {
return VideoNoteConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -158,7 +158,7 @@ func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig {
}
// NewVoice creates a new sendVoice request.
func NewVoice(chatID int64, file interface{}) VoiceConfig {
func NewVoice(chatID int64, file RequestFileData) VoiceConfig {
return VoiceConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
@ -177,7 +177,7 @@ func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig {
}
// NewInputMediaPhoto creates a new InputMediaPhoto.
func NewInputMediaPhoto(media interface{}) InputMediaPhoto {
func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto {
return InputMediaPhoto{
BaseInputMedia{
Type: "photo",
@ -187,7 +187,7 @@ func NewInputMediaPhoto(media interface{}) InputMediaPhoto {
}
// NewInputMediaVideo creates a new InputMediaVideo.
func NewInputMediaVideo(media interface{}) InputMediaVideo {
func NewInputMediaVideo(media RequestFileData) InputMediaVideo {
return InputMediaVideo{
BaseInputMedia: BaseInputMedia{
Type: "video",
@ -197,7 +197,7 @@ func NewInputMediaVideo(media interface{}) InputMediaVideo {
}
// NewInputMediaAnimation creates a new InputMediaAnimation.
func NewInputMediaAnimation(media interface{}) InputMediaAnimation {
func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation {
return InputMediaAnimation{
BaseInputMedia: BaseInputMedia{
Type: "animation",
@ -207,7 +207,7 @@ func NewInputMediaAnimation(media interface{}) InputMediaAnimation {
}
// NewInputMediaAudio creates a new InputMediaAudio.
func NewInputMediaAudio(media interface{}) InputMediaAudio {
func NewInputMediaAudio(media RequestFileData) InputMediaAudio {
return InputMediaAudio{
BaseInputMedia: BaseInputMedia{
Type: "audio",
@ -217,7 +217,7 @@ func NewInputMediaAudio(media interface{}) InputMediaAudio {
}
// NewInputMediaDocument creates a new InputMediaDocument.
func NewInputMediaDocument(media interface{}) InputMediaDocument {
func NewInputMediaDocument(media RequestFileData) InputMediaDocument {
return InputMediaDocument{
BaseInputMedia: BaseInputMedia{
Type: "document",
@ -316,7 +316,7 @@ func NewWebhook(link string) (WebhookConfig, error) {
//
// link is the url you wish to get webhooks,
// file contains a string to a file, FileReader, or FileBytes.
func NewWebhookWithCert(link string, file interface{}) (WebhookConfig, error) {
func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) {
u, err := url.Parse(link)
if err != nil {
@ -769,7 +769,7 @@ func NewChatDescription(chatID int64, description string) SetChatDescriptionConf
}
// NewChatPhoto allows you to update the photo for a chat.
func NewChatPhoto(chatID int64, photo interface{}) SetChatPhotoConfig {
func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig {
return SetChatPhotoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{
@ -781,7 +781,7 @@ func NewChatPhoto(chatID int64, photo interface{}) SetChatPhotoConfig {
}
// NewDeleteChatPhoto allows you to delete the photo for a chat.
func NewDeleteChatPhoto(chatID int64, photo interface{}) DeleteChatPhotoConfig {
func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig {
return DeleteChatPhotoConfig{
ChatID: chatID,
}

View File

@ -17,7 +17,7 @@ func TestNewWebhook(t *testing.T) {
}
func TestNewWebhookWithCert(t *testing.T) {
exampleFile := File{FileID: "123"}
exampleFile := FileID("123")
result, err := NewWebhookWithCert("https://example.com/token", exampleFile)
if err != nil ||

View File

@ -1723,7 +1723,7 @@ type BaseInputMedia struct {
// pass an HTTP URL for Telegram to get a file from the Internet,
// or pass “attach://<file_attach_name>” to upload a new one
// using multipart/form-data under <file_attach_name> name.
Media interface{} `json:"media"`
Media RequestFileData `json:"media"`
// thumb intentionally missing as it is not currently compatible
// Caption of the video to be sent, 0-1024 characters after entities parsing.
@ -1755,7 +1755,7 @@ type InputMediaVideo struct {
// the file is supported server-side.
//
// optional
Thumb interface{} `json:"thumb,omitempty"`
Thumb RequestFileData `json:"thumb,omitempty"`
// Width video width
//
// optional
@ -1781,7 +1781,7 @@ type InputMediaAnimation struct {
// the file is supported server-side.
//
// optional
Thumb interface{} `json:"thumb,omitempty"`
Thumb RequestFileData `json:"thumb,omitempty"`
// Width video width
//
// optional
@ -1803,7 +1803,7 @@ type InputMediaAudio struct {
// the file is supported server-side.
//
// optional
Thumb interface{} `json:"thumb,omitempty"`
Thumb RequestFileData `json:"thumb,omitempty"`
// Duration of the audio in seconds
//
// optional
@ -1825,7 +1825,7 @@ type InputMediaDocument struct {
// the file is supported server-side.
//
// optional
Thumb interface{} `json:"thumb,omitempty"`
Thumb RequestFileData `json:"thumb,omitempty"`
// DisableContentTypeDetection disables automatic server-side content type
// detection for files uploaded using multipart/form-data. Always true, if
// the document is sent as part of an album

View File

@ -361,3 +361,13 @@ var (
_ Fileable = (*WebhookConfig)(nil)
_ Fileable = (*SetStickerSetThumbConfig)(nil)
)
// Ensure all RequestFileData types are correct.
var (
_ RequestFileData = (*FilePath)(nil)
_ RequestFileData = (*FileBytes)(nil)
_ RequestFileData = (*FileReader)(nil)
_ RequestFileData = (*FileURL)(nil)
_ RequestFileData = (*FileID)(nil)
_ RequestFileData = (*fileAttach)(nil)
)