From a8e2c08cb5dbd25c38f5cea578a8d5027337ef21 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Mon, 1 Jul 2024 20:41:58 +0200 Subject: [PATCH] Compile-time inputfile validation (#163) * Cleanup and improve inputfile interfaces * Update sample bots to work with new typesafe inputfile logic * Media should be of type inputfileorstring * fix lint * Make sure thumbnails cannot be strings * Improve inputfile logic to properly handle values json value marshalling * Add missing thumbnail logic to certain inputmedia types and improve error messages * remove unnecessary InputString type which turned out to be unnecessary * regenerate after merge --- bot.go | 2 +- file.go | 94 +++++ gen_methods.go | 435 ++++++------------------ gen_types.go | 240 +++++-------- request.go | 28 +- samples/commandBot/main.go | 70 ++-- samples/metricsBot/metricsMiddleware.go | 2 +- samples/middlewareBot/middleware.go | 2 +- scripts/generate/gen.go | 23 +- scripts/generate/methods.go | 64 +--- scripts/generate/types.go | 63 ++-- 11 files changed, 382 insertions(+), 641 deletions(-) create mode 100644 file.go diff --git a/bot.go b/bot.go index 6c63e792..bce9791a 100644 --- a/bot.go +++ b/bot.go @@ -90,7 +90,7 @@ func (bot *Bot) UseMiddleware(mw func(client BotClient) BotClient) *Bot { var ErrNilBotClient = errors.New("nil BotClient") -func (bot *Bot) Request(method string, params map[string]string, data map[string]NamedReader, opts *RequestOpts) (json.RawMessage, error) { +func (bot *Bot) Request(method string, params map[string]string, data map[string]FileReader, opts *RequestOpts) (json.RawMessage, error) { if bot.BotClient == nil { return nil, ErrNilBotClient } diff --git a/file.go b/file.go new file mode 100644 index 00000000..bb2a9836 --- /dev/null +++ b/file.go @@ -0,0 +1,94 @@ +package gotgbot + +import ( + "encoding/json" + "errors" + "io" +) + +// InputFile (https://core.telegram.org/bots/api#inputfile) +// +// This object represents the contents of a file to be uploaded. +// Must be posted using multipart/form-data in the usual way that files are uploaded via the browser. +type InputFile interface { + InputFileOrString + justFiles() +} + +// InputFileOrString (https://core.telegram.org/bots/api#inputfile) +// +// This object represents the contents of a file to be uploaded, or a publicly accessible URL to be reused. +// Files must be posted using multipart/form-data in the usual way that files are uploaded via the browser. +type InputFileOrString interface { + Attach(name string, data map[string]FileReader) error + getValue() string +} + +var ( + _ InputFileOrString = &FileReader{} + _ InputFile = &FileReader{} +) + +type FileReader struct { + Name string + Data io.Reader + + value string +} + +func (f *FileReader) MarshalJSON() ([]byte, error) { + return json.Marshal(f.getValue()) +} + +var ErrAttachmentKeyAlreadyExists = errors.New("key already exists") + +func (f *FileReader) justFiles() {} + +func (f *FileReader) Attach(key string, data map[string]FileReader) error { + if f.Data == nil { + // if no data, this must be a string; nothing to "attach". + return nil + } + + if _, ok := data[key]; ok { + return ErrAttachmentKeyAlreadyExists + } + f.value = "attach://" + key + data[key] = *f + return nil +} + +// getValue returns the file attach reference for the relevant multipart form. +// Make sure to only call getValue after having called Attach(), to ensure any files have been included. +func (f *FileReader) getValue() string { + return f.value +} + +// InputFileByURL is used to send a file on the internet via a publicly accessible HTTP URL. +func InputFileByURL(url string) InputFileOrString { + return &FileReader{value: url} +} + +// InputFileByID is used to send a file that is already present on telegram's servers, using its telegram file_id. +func InputFileByID(fileID string) InputFileOrString { + return &FileReader{value: fileID} +} + +// InputFileByReader is used to send a file by a reader interface; such as a filehandle from os.Open(), or from a byte +// buffer. +// +// For example: +// +// f, err := os.Open("some_file.go") +// if err != nil { +// return fmt.Errorf("failed to open file: %w", err) +// } +// +// m, err := b.SendDocument(, gotgbot.InputFileByReader("source.go", f), nil) +// +// Or +// +// m, err := b.SendDocument(, gotgbot.InputFileByReader("file.txt", strings.NewReader("Some file contents")), nil) +func InputFileByReader(name string, r io.Reader) InputFile { + return &FileReader{Name: name, Data: r} +} diff --git a/gen_methods.go b/gen_methods.go index 4d2cd3bf..e6f15135 100755 --- a/gen_methods.go +++ b/gen_methods.go @@ -4,10 +4,8 @@ package gotgbot import ( - "bytes" "encoding/json" "fmt" - "io" "strconv" ) @@ -26,7 +24,7 @@ type AddStickerToSetOpts struct { // - opts (type AddStickerToSetOpts): All optional parameters. func (bot *Bot) AddStickerToSet(userId int64, name string, sticker InputSticker, opts *AddStickerToSetOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["user_id"] = strconv.FormatInt(userId, 10) v["name"] = name inputBs, err := sticker.InputParams("sticker", data) @@ -811,7 +809,7 @@ type CreateNewStickerSetOpts struct { // - opts (type CreateNewStickerSetOpts): All optional parameters. func (bot *Bot) CreateNewStickerSet(userId int64, name string, title string, stickers []InputSticker, opts *CreateNewStickerSetOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["user_id"] = strconv.FormatInt(userId, 10) v["name"] = name v["title"] = title @@ -1482,7 +1480,7 @@ type EditMessageMediaOpts struct { // - opts (type EditMessageMediaOpts): All optional parameters. func (bot *Bot) EditMessageMedia(media InputMedia, opts *EditMessageMediaOpts) (*Message, bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} inputBs, err := media.InputParams("media", data) if err != nil { return nil, false, fmt.Errorf("failed to marshal field media: %w", err) @@ -2830,7 +2828,7 @@ type ReplaceStickerInSetOpts struct { // - opts (type ReplaceStickerInSetOpts): All optional parameters. func (bot *Bot) ReplaceStickerInSet(userId int64, name string, oldSticker string, sticker InputSticker, opts *ReplaceStickerInSetOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["user_id"] = strconv.FormatInt(userId, 10) v["name"] = name v["old_sticker"] = oldSticker @@ -2974,32 +2972,18 @@ type SendAnimationOpts struct { // // Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent Message is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. // - chatId (type int64): Unique identifier for the target chat -// - animation (type InputFile): Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - animation (type InputFileOrString): Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendAnimationOpts): All optional parameters. -func (bot *Bot) SendAnimation(chatId int64, animation InputFile, opts *SendAnimationOpts) (*Message, error) { +func (bot *Bot) SendAnimation(chatId int64, animation InputFileOrString, opts *SendAnimationOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if animation != nil { - switch m := animation.(type) { - case string: - v["animation"] = m - - case NamedReader: - v["animation"] = "attach://animation" - data["animation"] = m - - case io.Reader: - v["animation"] = "attach://animation" - data["animation"] = NamedFile{File: m} - - case []byte: - v["animation"] = "attach://animation" - data["animation"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", animation) + err := animation.Attach("animation", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'animation' input file: %w", err) } + v["animation"] = animation.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -3016,25 +3000,11 @@ func (bot *Bot) SendAnimation(chatId int64, animation InputFile, opts *SendAnima v["height"] = strconv.FormatInt(opts.Height, 10) } if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } v["caption"] = opts.Caption v["parse_mode"] = opts.ParseMode @@ -3119,32 +3089,18 @@ type SendAudioOpts struct { // Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent Message is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. // For sending voice messages, use the sendVoice method instead. // - chatId (type int64): Unique identifier for the target chat -// - audio (type InputFile): Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - audio (type InputFileOrString): Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendAudioOpts): All optional parameters. -func (bot *Bot) SendAudio(chatId int64, audio InputFile, opts *SendAudioOpts) (*Message, error) { +func (bot *Bot) SendAudio(chatId int64, audio InputFileOrString, opts *SendAudioOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if audio != nil { - switch m := audio.(type) { - case string: - v["audio"] = m - - case NamedReader: - v["audio"] = "attach://audio" - data["audio"] = m - - case io.Reader: - v["audio"] = "attach://audio" - data["audio"] = NamedFile{File: m} - - case []byte: - v["audio"] = "attach://audio" - data["audio"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", audio) + err := audio.Attach("audio", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'audio' input file: %w", err) } + v["audio"] = audio.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -3166,25 +3122,11 @@ func (bot *Bot) SendAudio(chatId int64, audio InputFile, opts *SendAudioOpts) (* v["performer"] = opts.Performer v["title"] = opts.Title if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } v["disable_notification"] = strconv.FormatBool(opts.DisableNotification) v["protect_content"] = strconv.FormatBool(opts.ProtectContent) @@ -3440,32 +3382,18 @@ type SendDocumentOpts struct { // // Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. // - chatId (type int64): Unique identifier for the target chat -// - document (type InputFile): File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - document (type InputFileOrString): File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendDocumentOpts): All optional parameters. -func (bot *Bot) SendDocument(chatId int64, document InputFile, opts *SendDocumentOpts) (*Message, error) { +func (bot *Bot) SendDocument(chatId int64, document InputFileOrString, opts *SendDocumentOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if document != nil { - switch m := document.(type) { - case string: - v["document"] = m - - case NamedReader: - v["document"] = "attach://document" - data["document"] = m - - case io.Reader: - v["document"] = "attach://document" - data["document"] = NamedFile{File: m} - - case []byte: - v["document"] = "attach://document" - data["document"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", document) + err := document.Attach("document", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'document' input file: %w", err) } + v["document"] = document.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -3473,25 +3401,11 @@ func (bot *Bot) SendDocument(chatId int64, document InputFile, opts *SendDocumen v["message_thread_id"] = strconv.FormatInt(opts.MessageThreadId, 10) } if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } v["caption"] = opts.Caption v["parse_mode"] = opts.ParseMode @@ -3857,7 +3771,7 @@ type SendMediaGroupOpts struct { // - opts (type SendMediaGroupOpts): All optional parameters. func (bot *Bot) SendMediaGroup(chatId int64, media []InputMedia, opts *SendMediaGroupOpts) ([]Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if media != nil { var rawList []json.RawMessage @@ -4025,7 +3939,7 @@ type SendPaidMediaOpts struct { // - opts (type SendPaidMediaOpts): All optional parameters. func (bot *Bot) SendPaidMedia(chatId int64, starCount int64, media []InputPaidMedia, opts *SendPaidMediaOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) v["star_count"] = strconv.FormatInt(starCount, 10) if media != nil { @@ -4120,32 +4034,18 @@ type SendPhotoOpts struct { // // Use this method to send photos. On success, the sent Message is returned. // - chatId (type int64): Unique identifier for the target chat -// - photo (type InputFile): Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. The photo must be at most 10 MB in size. The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - photo (type InputFileOrString): Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. The photo must be at most 10 MB in size. The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendPhotoOpts): All optional parameters. -func (bot *Bot) SendPhoto(chatId int64, photo InputFile, opts *SendPhotoOpts) (*Message, error) { +func (bot *Bot) SendPhoto(chatId int64, photo InputFileOrString, opts *SendPhotoOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if photo != nil { - switch m := photo.(type) { - case string: - v["photo"] = m - - case NamedReader: - v["photo"] = "attach://photo" - data["photo"] = m - - case io.Reader: - v["photo"] = "attach://photo" - data["photo"] = NamedFile{File: m} - - case []byte: - v["photo"] = "attach://photo" - data["photo"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", photo) + err := photo.Attach("photo", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'photo' input file: %w", err) } + v["photo"] = photo.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -4353,32 +4253,18 @@ type SendStickerOpts struct { // // Use this method to send static .WEBP, animated .TGS, or video .WEBM stickers. On success, the sent Message is returned. // - chatId (type int64): Unique identifier for the target chat -// - sticker (type InputFile): Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP sticker from the Internet, or upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files. Video and animated stickers can't be sent via an HTTP URL. +// - sticker (type InputFileOrString): Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP sticker from the Internet, or upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files. Video and animated stickers can't be sent via an HTTP URL. // - opts (type SendStickerOpts): All optional parameters. -func (bot *Bot) SendSticker(chatId int64, sticker InputFile, opts *SendStickerOpts) (*Message, error) { +func (bot *Bot) SendSticker(chatId int64, sticker InputFileOrString, opts *SendStickerOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if sticker != nil { - switch m := sticker.(type) { - case string: - v["sticker"] = m - - case NamedReader: - v["sticker"] = "attach://sticker" - data["sticker"] = m - - case io.Reader: - v["sticker"] = "attach://sticker" - data["sticker"] = NamedFile{File: m} - - case []byte: - v["sticker"] = "attach://sticker" - data["sticker"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", sticker) + err := sticker.Attach("sticker", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'sticker' input file: %w", err) } + v["sticker"] = sticker.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -4549,32 +4435,18 @@ type SendVideoOpts struct { // // Use this method to send video files, Telegram clients support MPEG4 videos (other formats may be sent as Document). On success, the sent Message is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. // - chatId (type int64): Unique identifier for the target chat -// - video (type InputFile): Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - video (type InputFileOrString): Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendVideoOpts): All optional parameters. -func (bot *Bot) SendVideo(chatId int64, video InputFile, opts *SendVideoOpts) (*Message, error) { +func (bot *Bot) SendVideo(chatId int64, video InputFileOrString, opts *SendVideoOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if video != nil { - switch m := video.(type) { - case string: - v["video"] = m - - case NamedReader: - v["video"] = "attach://video" - data["video"] = m - - case io.Reader: - v["video"] = "attach://video" - data["video"] = NamedFile{File: m} - - case []byte: - v["video"] = "attach://video" - data["video"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", video) + err := video.Attach("video", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'video' input file: %w", err) } + v["video"] = video.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -4591,25 +4463,11 @@ func (bot *Bot) SendVideo(chatId int64, video InputFile, opts *SendVideoOpts) (* v["height"] = strconv.FormatInt(opts.Height, 10) } if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } v["caption"] = opts.Caption v["parse_mode"] = opts.ParseMode @@ -4686,32 +4544,18 @@ type SendVideoNoteOpts struct { // // As of v.4.0, Telegram clients support rounded square MPEG4 videos of up to 1 minute long. Use this method to send video messages. On success, the sent Message is returned. // - chatId (type int64): Unique identifier for the target chat -// - videoNote (type InputFile): Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files. Sending video notes by a URL is currently unsupported +// - videoNote (type InputFileOrString): Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files. Sending video notes by a URL is currently unsupported // - opts (type SendVideoNoteOpts): All optional parameters. -func (bot *Bot) SendVideoNote(chatId int64, videoNote InputFile, opts *SendVideoNoteOpts) (*Message, error) { +func (bot *Bot) SendVideoNote(chatId int64, videoNote InputFileOrString, opts *SendVideoNoteOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if videoNote != nil { - switch m := videoNote.(type) { - case string: - v["video_note"] = m - - case NamedReader: - v["video_note"] = "attach://video_note" - data["video_note"] = m - - case io.Reader: - v["video_note"] = "attach://video_note" - data["video_note"] = NamedFile{File: m} - - case []byte: - v["video_note"] = "attach://video_note" - data["video_note"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", videoNote) + err := videoNote.Attach("video_note", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'video_note' input file: %w", err) } + v["video_note"] = videoNote.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -4725,25 +4569,11 @@ func (bot *Bot) SendVideoNote(chatId int64, videoNote InputFile, opts *SendVideo v["length"] = strconv.FormatInt(opts.Length, 10) } if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } v["disable_notification"] = strconv.FormatBool(opts.DisableNotification) v["protect_content"] = strconv.FormatBool(opts.ProtectContent) @@ -4810,32 +4640,18 @@ type SendVoiceOpts struct { // // Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS, or in .MP3 format, or in .M4A format (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. // - chatId (type int64): Unique identifier for the target chat -// - voice (type InputFile): Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files +// - voice (type InputFileOrString): Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files: https://core.telegram.org/bots/api#sending-files // - opts (type SendVoiceOpts): All optional parameters. -func (bot *Bot) SendVoice(chatId int64, voice InputFile, opts *SendVoiceOpts) (*Message, error) { +func (bot *Bot) SendVoice(chatId int64, voice InputFileOrString, opts *SendVoiceOpts) (*Message, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if voice != nil { - switch m := voice.(type) { - case string: - v["voice"] = m - - case NamedReader: - v["voice"] = "attach://voice" - data["voice"] = m - - case io.Reader: - v["voice"] = "attach://voice" - data["voice"] = NamedFile{File: m} - - case []byte: - v["voice"] = "attach://voice" - data["voice"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", voice) + err := voice.Attach("voice", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'voice' input file: %w", err) } + v["voice"] = voice.getValue() } if opts != nil { v["business_connection_id"] = opts.BusinessConnectionId @@ -5049,25 +4865,14 @@ type SetChatPhotoOpts struct { // - opts (type SetChatPhotoOpts): All optional parameters. func (bot *Bot) SetChatPhoto(chatId int64, photo InputFile, opts *SetChatPhotoOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["chat_id"] = strconv.FormatInt(chatId, 10) if photo != nil { - switch m := photo.(type) { - case NamedReader: - v["photo"] = "attach://photo" - data["photo"] = m - - case io.Reader: - v["photo"] = "attach://photo" - data["photo"] = NamedFile{File: m} - - case []byte: - v["photo"] = "attach://photo" - data["photo"] = NamedFile{File: bytes.NewReader(m)} - - default: - return false, fmt.Errorf("unknown type for InputFile: %T", photo) + err := photo.Attach("photo", data) + if err != nil { + return false, fmt.Errorf("failed to attach 'photo' input file: %w", err) } + v["photo"] = photo.getValue() } var reqOpts *RequestOpts @@ -5681,31 +5486,17 @@ type SetStickerSetThumbnailOpts struct { // - opts (type SetStickerSetThumbnailOpts): All optional parameters. func (bot *Bot) SetStickerSetThumbnail(name string, userId int64, format string, opts *SetStickerSetThumbnailOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["name"] = name v["user_id"] = strconv.FormatInt(userId, 10) v["format"] = format if opts != nil { if opts.Thumbnail != nil { - switch m := opts.Thumbnail.(type) { - case string: - v["thumbnail"] = m - - case NamedReader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = m - - case io.Reader: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: m} - - case []byte: - v["thumbnail"] = "attach://thumbnail" - data["thumbnail"] = NamedFile{File: bytes.NewReader(m)} - - default: - return false, fmt.Errorf("unknown type for InputFile: %T", opts.Thumbnail) + err := opts.Thumbnail.Attach("thumbnail", data) + if err != nil { + return false, fmt.Errorf("failed to attach 'thumbnail' input file: %w", err) } + v["thumbnail"] = opts.Thumbnail.getValue() } } @@ -5780,26 +5571,15 @@ type SetWebhookOpts struct { // - opts (type SetWebhookOpts): All optional parameters. func (bot *Bot) SetWebhook(url string, opts *SetWebhookOpts) (bool, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["url"] = url if opts != nil { if opts.Certificate != nil { - switch m := opts.Certificate.(type) { - case NamedReader: - v["certificate"] = "attach://certificate" - data["certificate"] = m - - case io.Reader: - v["certificate"] = "attach://certificate" - data["certificate"] = NamedFile{File: m} - - case []byte: - v["certificate"] = "attach://certificate" - data["certificate"] = NamedFile{File: bytes.NewReader(m)} - - default: - return false, fmt.Errorf("unknown type for InputFile: %T", opts.Certificate) + err := opts.Certificate.Attach("certificate", data) + if err != nil { + return false, fmt.Errorf("failed to attach 'certificate' input file: %w", err) } + v["certificate"] = opts.Certificate.getValue() } v["ip_address"] = opts.IpAddress if opts.MaxConnections != 0 { @@ -6169,25 +5949,14 @@ type UploadStickerFileOpts struct { // - opts (type UploadStickerFileOpts): All optional parameters. func (bot *Bot) UploadStickerFile(userId int64, sticker InputFile, stickerFormat string, opts *UploadStickerFileOpts) (*File, error) { v := map[string]string{} - data := map[string]NamedReader{} + data := map[string]FileReader{} v["user_id"] = strconv.FormatInt(userId, 10) if sticker != nil { - switch m := sticker.(type) { - case NamedReader: - v["sticker"] = "attach://sticker" - data["sticker"] = m - - case io.Reader: - v["sticker"] = "attach://sticker" - data["sticker"] = NamedFile{File: m} - - case []byte: - v["sticker"] = "attach://sticker" - data["sticker"] = NamedFile{File: bytes.NewReader(m)} - - default: - return nil, fmt.Errorf("unknown type for InputFile: %T", sticker) + err := sticker.Attach("sticker", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'sticker' input file: %w", err) } + v["sticker"] = sticker.getValue() } v["sticker_format"] = stickerFormat diff --git a/gen_types.go b/gen_types.go index 5ec33827..e2651e65 100755 --- a/gen_types.go +++ b/gen_types.go @@ -6,7 +6,6 @@ package gotgbot import ( "encoding/json" "fmt" - "io" ) type ReplyMarkup interface { @@ -4604,8 +4603,6 @@ func (v InputContactMessageContent) inputMessageContent() {} // InputFile (https://core.telegram.org/bots/api#inputfile) // // This object represents the contents of a file to be uploaded. Must be posted using multipart/form-data in the usual way that files are uploaded via the browser. -type InputFile interface{} - // InputInvoiceMessageContent (https://core.telegram.org/bots/api#inputinvoicemessagecontent) // // Represents the content of an invoice message to be sent as the result of an inline query. @@ -4686,9 +4683,9 @@ func (v InputLocationMessageContent) inputMessageContent() {} // - InputMediaVideo type InputMedia interface { GetType() string - GetMedia() InputFile + GetMedia() InputFileOrString // InputParams allows for uploading attachments with files. - InputParams(string, map[string]NamedReader) ([]byte, error) + InputParams(string, map[string]FileReader) ([]byte, error) // MergeInputMedia returns a MergedInputMedia struct to simplify working with complex telegram types in a non-generic world. MergeInputMedia() MergedInputMedia // inputMedia exists to avoid external types implementing this interface. @@ -4709,9 +4706,9 @@ type MergedInputMedia struct { // Type of the result Type string `json:"type"` // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files (Only for animation, document, audio, video) - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Caption of the animation to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the animation caption. See formatting options for more details. @@ -4744,7 +4741,7 @@ func (v MergedInputMedia) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v MergedInputMedia) GetMedia() InputFile { +func (v MergedInputMedia) GetMedia() InputFileOrString { return v.Media } @@ -4761,9 +4758,9 @@ func (v MergedInputMedia) MergeInputMedia() MergedInputMedia { // Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. type InputMediaAnimation struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Caption of the animation to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the animation caption. See formatting options for more details. @@ -4788,7 +4785,7 @@ func (v InputMediaAnimation) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputMediaAnimation) GetMedia() InputFile { +func (v InputMediaAnimation) GetMedia() InputFileOrString { return v.Media } @@ -4825,22 +4822,18 @@ func (v InputMediaAnimation) MarshalJSON() ([]byte, error) { // InputMediaAnimation.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaAnimation) inputMedia() {} -func (v InputMediaAnimation) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputMediaAnimation) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) + } + } - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) } } @@ -4852,9 +4845,9 @@ func (v InputMediaAnimation) InputParams(mediaName string, data map[string]Named // Represents an audio file to be treated as music to be sent. type InputMediaAudio struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Caption of the audio to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the audio caption. See formatting options for more details. @@ -4875,7 +4868,7 @@ func (v InputMediaAudio) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputMediaAudio) GetMedia() InputFile { +func (v InputMediaAudio) GetMedia() InputFileOrString { return v.Media } @@ -4910,22 +4903,18 @@ func (v InputMediaAudio) MarshalJSON() ([]byte, error) { // InputMediaAudio.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaAudio) inputMedia() {} -func (v InputMediaAudio) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputMediaAudio) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) + } + } - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) } } @@ -4937,9 +4926,9 @@ func (v InputMediaAudio) InputParams(mediaName string, data map[string]NamedRead // Represents a general file to be sent. type InputMediaDocument struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Caption of the document to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the document caption. See formatting options for more details. @@ -4956,7 +4945,7 @@ func (v InputMediaDocument) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputMediaDocument) GetMedia() InputFile { +func (v InputMediaDocument) GetMedia() InputFileOrString { return v.Media } @@ -4989,22 +4978,18 @@ func (v InputMediaDocument) MarshalJSON() ([]byte, error) { // InputMediaDocument.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaDocument) inputMedia() {} -func (v InputMediaDocument) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputMediaDocument) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) + } + } - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) } } @@ -5016,7 +5001,7 @@ func (v InputMediaDocument) InputParams(mediaName string, data map[string]NamedR // Represents a photo to be sent. type InputMediaPhoto struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the photo caption. See formatting options for more details. @@ -5035,7 +5020,7 @@ func (v InputMediaPhoto) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputMediaPhoto) GetMedia() InputFile { +func (v InputMediaPhoto) GetMedia() InputFileOrString { return v.Media } @@ -5068,22 +5053,11 @@ func (v InputMediaPhoto) MarshalJSON() ([]byte, error) { // InputMediaPhoto.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaPhoto) inputMedia() {} -func (v InputMediaPhoto) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputMediaPhoto) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} - - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) } } @@ -5095,9 +5069,9 @@ func (v InputMediaPhoto) InputParams(mediaName string, data map[string]NamedRead // Represents a video to be sent. type InputMediaVideo struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Caption of the video to be sent, 0-1024 characters after entities parsing Caption string `json:"caption,omitempty"` // Optional. Mode for parsing entities in the video caption. See formatting options for more details. @@ -5124,7 +5098,7 @@ func (v InputMediaVideo) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputMediaVideo) GetMedia() InputFile { +func (v InputMediaVideo) GetMedia() InputFileOrString { return v.Media } @@ -5162,22 +5136,18 @@ func (v InputMediaVideo) MarshalJSON() ([]byte, error) { // InputMediaVideo.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaVideo) inputMedia() {} -func (v InputMediaVideo) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputMediaVideo) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) + } + } - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) } } @@ -5213,9 +5183,9 @@ var ( // - InputPaidMediaVideo type InputPaidMedia interface { GetType() string - GetMedia() InputFile + GetMedia() InputFileOrString // InputParams allows for uploading attachments with files. - InputParams(string, map[string]NamedReader) ([]byte, error) + InputParams(string, map[string]FileReader) ([]byte, error) // MergeInputPaidMedia returns a MergedInputPaidMedia struct to simplify working with complex telegram types in a non-generic world. MergeInputPaidMedia() MergedInputPaidMedia // inputPaidMedia exists to avoid external types implementing this interface. @@ -5233,9 +5203,9 @@ type MergedInputPaidMedia struct { // Type of the media Type string `json:"type"` // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files (Only for video) - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Video width (Only for video) Width int64 `json:"width,omitempty"` // Optional. Video height (Only for video) @@ -5252,7 +5222,7 @@ func (v MergedInputPaidMedia) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v MergedInputPaidMedia) GetMedia() InputFile { +func (v MergedInputPaidMedia) GetMedia() InputFileOrString { return v.Media } @@ -5269,7 +5239,7 @@ func (v MergedInputPaidMedia) MergeInputPaidMedia() MergedInputPaidMedia { // The paid media to send is a photo. type InputPaidMediaPhoto struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` } // GetType is a helper method to easily access the common fields of an interface. @@ -5278,7 +5248,7 @@ func (v InputPaidMediaPhoto) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputPaidMediaPhoto) GetMedia() InputFile { +func (v InputPaidMediaPhoto) GetMedia() InputFileOrString { return v.Media } @@ -5306,22 +5276,11 @@ func (v InputPaidMediaPhoto) MarshalJSON() ([]byte, error) { // InputPaidMediaPhoto.inputPaidMedia is a dummy method to avoid interface implementation. func (v InputPaidMediaPhoto) inputPaidMedia() {} -func (v InputPaidMediaPhoto) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputPaidMediaPhoto) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} - - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) } } @@ -5333,9 +5292,9 @@ func (v InputPaidMediaPhoto) InputParams(mediaName string, data map[string]Named // The paid media to send is a video. type InputPaidMediaVideo struct { // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Media InputFile `json:"media"` + Media InputFileOrString `json:"media"` // Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Thumbnail *InputFile `json:"thumbnail,omitempty"` + Thumbnail InputFile `json:"thumbnail,omitempty"` // Optional. Video width Width int64 `json:"width,omitempty"` // Optional. Video height @@ -5352,7 +5311,7 @@ func (v InputPaidMediaVideo) GetType() string { } // GetMedia is a helper method to easily access the common fields of an interface. -func (v InputPaidMediaVideo) GetMedia() InputFile { +func (v InputPaidMediaVideo) GetMedia() InputFileOrString { return v.Media } @@ -5385,22 +5344,18 @@ func (v InputPaidMediaVideo) MarshalJSON() ([]byte, error) { // InputPaidMediaVideo.inputPaidMedia is a dummy method to avoid interface implementation. func (v InputPaidMediaVideo) inputPaidMedia() {} -func (v InputPaidMediaVideo) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputPaidMediaVideo) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Media != nil { - switch m := v.Media.(type) { - case string: - // ok, noop - - case NamedReader: - v.Media = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Media = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} + err := v.Media.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) + } + } - default: - return nil, fmt.Errorf("unknown type: %T", v.Media) + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) } } @@ -5424,7 +5379,7 @@ type InputPollOption struct { // This object describes a sticker to be added to a sticker set. type InputSticker struct { // The added sticker. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, upload a new one using multipart/form-data, or pass "attach://" to upload a new one using multipart/form-data under name. Animated and video stickers can't be uploaded via HTTP URL. More information on Sending Files: https://core.telegram.org/bots/api#sending-files - Sticker InputFile `json:"sticker"` + Sticker InputFileOrString `json:"sticker"` // Format of the added sticker, must be one of "static" for a .WEBP or .PNG image, "animated" for a .TGS animation, "video" for a WEBM video Format string `json:"format"` // List of 1-20 emoji associated with the sticker @@ -5435,22 +5390,11 @@ type InputSticker struct { Keywords []string `json:"keywords,omitempty"` } -func (v InputSticker) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v InputSticker) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.Sticker != nil { - switch m := v.Sticker.(type) { - case string: - // ok, noop - - case NamedReader: - v.Sticker = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.Sticker = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} - - default: - return nil, fmt.Errorf("unknown type: %T", v.Sticker) + err := v.Sticker.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) } } diff --git a/request.go b/request.go index 1dafe456..893abf83 100644 --- a/request.go +++ b/request.go @@ -21,7 +21,7 @@ const ( type BotClient interface { // RequestWithContext submits a POST HTTP request a bot API instance. - RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]NamedReader, opts *RequestOpts) (json.RawMessage, error) + RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]FileReader, opts *RequestOpts) (json.RawMessage, error) // TimeoutContext calculates the required timeout contect required given the passed RequestOpts, and any default opts defined by the BotClient. TimeoutContext(opts *RequestOpts) (context.Context, context.CancelFunc) // GetAPIURL gets the URL of the API either in use by the bot or defined in the request opts. @@ -74,24 +74,6 @@ func (t *TelegramError) Error() string { return fmt.Sprintf("unable to %s: %s", t.Method, t.Description) } -type NamedReader interface { - Name() string - io.Reader -} - -type NamedFile struct { - File io.Reader - FileName string -} - -func (nf NamedFile) Read(p []byte) (n int, err error) { - return nf.File.Read(p) -} - -func (nf NamedFile) Name() string { - return nf.FileName -} - // RequestOpts defines any request-specific options used to interact with the telegram API. type RequestOpts struct { // Timeout for the HTTP request to the telegram API. @@ -144,7 +126,7 @@ func timeoutFromOpts(opts *RequestOpts) (context.Context, context.CancelFunc) { // - data: map of any files to be sending to the telegram API. // - opts: request opts to use. Note: Timeout opts are ignored when used in RequestWithContext. Timeout handling is the // responsibility of the caller/context owner. -func (bot *BaseBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]NamedReader, opts *RequestOpts) (json.RawMessage, error) { +func (bot *BaseBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]FileReader, opts *RequestOpts) (json.RawMessage, error) { b := &bytes.Buffer{} var contentType string @@ -194,7 +176,7 @@ func (bot *BaseBotClient) RequestWithContext(ctx context.Context, token string, return r.Result, nil } -func fillBuffer(b *bytes.Buffer, params map[string]string, data map[string]NamedReader) (string, error) { +func fillBuffer(b *bytes.Buffer, params map[string]string, data map[string]FileReader) (string, error) { w := multipart.NewWriter(b) for k, v := range params { @@ -205,7 +187,7 @@ func fillBuffer(b *bytes.Buffer, params map[string]string, data map[string]Named } for field, file := range data { - fileName := file.Name() + fileName := file.Name if fileName == "" { fileName = field } @@ -215,7 +197,7 @@ func fillBuffer(b *bytes.Buffer, params map[string]string, data map[string]Named return "", fmt.Errorf("failed to create form file for field %s and fileName %s: %w", field, fileName, err) } - _, err = io.Copy(part, file) + _, err = io.Copy(part, file.Data) if err != nil { return "", fmt.Errorf("failed to copy file contents of field %s to form: %w", field, err) } diff --git a/samples/commandBot/main.go b/samples/commandBot/main.go index 11217e98..0062ad91 100644 --- a/samples/commandBot/main.go +++ b/samples/commandBot/main.go @@ -63,64 +63,36 @@ func main() { } func source(b *gotgbot.Bot, ctx *ext.Context) error { + // Sending a file by file handle f, err := os.Open("samples/commandBot/main.go") if err != nil { return fmt.Errorf("failed to open source: %w", err) } - _, err = b.SendDocument(ctx.EffectiveChat.Id, f, &gotgbot.SendDocumentOpts{ - Caption: "Here is my source code.", - ReplyParameters: &gotgbot.ReplyParameters{ - MessageId: ctx.EffectiveMessage.MessageId, - }, - }) + m, err := b.SendDocument(ctx.EffectiveChat.Id, + gotgbot.InputFileByReader("source.go", f), + &gotgbot.SendDocumentOpts{ + Caption: "Here is my source code, by file handle.", + ReplyParameters: &gotgbot.ReplyParameters{ + MessageId: ctx.EffectiveMessage.MessageId, + }, + }) if err != nil { return fmt.Errorf("failed to send source: %w", err) } - // Alternative file sending solutions: - - // --- By file_id: - // _, err = ctx.Bot.SendDocument(ctx.EffectiveChat.Id, "file_id", &gotgbot.SendDocumentOpts{ - // Caption: "Here is my source code.", - // ReplyToMessageId: ctx.EffectiveMessage.MessageId, - // }) - // if err != nil { - // return fmt.Errorf("failed to send source: %w", err) - // } - - // --- By []byte: - // bs, err := ioutil.ReadFile("samples/commandBot/main.go") - // if err != nil { - // return fmt.Errorf("failed to open source: %w", err) - // } - // - // _, err = ctx.Bot.SendDocument(ctx.EffectiveChat.Id, bs, &gotgbot.SendDocumentOpts{ - // Caption: "Here is my source code.", - // ReplyToMessageId: ctx.EffectiveMessage.MessageId, - // }) - // if err != nil { - // return fmt.Errorf("failed to send source: %w", err) - // } - - // --- By custom name: - // f2, err := os.Open("samples/commandBot/main.go") - // if err != nil { - // return fmt.Errorf("failed to open source: %w", err) - // return err - // } - // - // _, err = ctx.Bot.SendDocument(ctx.EffectiveChat.Id, gotgbot.NamedFile{ - // File: f2, - // FileName: "NewFileName", - // }, &gotgbot.SendDocumentOpts{ - // Caption: "Here is my source code.", - // ReplyToMessageId: ctx.EffectiveMessage.MessageId, - // }) - // if err != nil { - // return fmt.Errorf("failed to send source: %w", err) - // return err - // } + // Or sending a file by file ID + _, err = b.SendDocument(ctx.EffectiveChat.Id, + gotgbot.InputFileByID(m.Document.FileId), + &gotgbot.SendDocumentOpts{ + Caption: "Here is my source code, sent by file id.", + ReplyParameters: &gotgbot.ReplyParameters{ + MessageId: ctx.EffectiveMessage.MessageId, + }, + }) + if err != nil { + return fmt.Errorf("failed to send source: %w", err) + } return nil } diff --git a/samples/metricsBot/metricsMiddleware.go b/samples/metricsBot/metricsMiddleware.go index 5984178d..24e19ab7 100644 --- a/samples/metricsBot/metricsMiddleware.go +++ b/samples/metricsBot/metricsMiddleware.go @@ -67,7 +67,7 @@ type metricsBotClient struct { // Define wrapper around existing RequestWithContext method. // Note: this is the only method that needs redefining. -func (b metricsBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]gotgbot.NamedReader, opts *gotgbot.RequestOpts) (json.RawMessage, error) { +func (b metricsBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]gotgbot.FileReader, opts *gotgbot.RequestOpts) (json.RawMessage, error) { totalRequests.WithLabelValues(method).Inc() timer := prometheus.NewTimer(requestDuration.With(prometheus.Labels{ "api_method": method, diff --git a/samples/middlewareBot/middleware.go b/samples/middlewareBot/middleware.go index 67c419c7..5949cb69 100644 --- a/samples/middlewareBot/middleware.go +++ b/samples/middlewareBot/middleware.go @@ -19,7 +19,7 @@ type sendWithoutReplyBotClient struct { // Define wrapper around existing RequestWithContext method. // Note: this is the only method that needs redefining. -func (b sendWithoutReplyBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]gotgbot.NamedReader, opts *gotgbot.RequestOpts) (json.RawMessage, error) { +func (b sendWithoutReplyBotClient) RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]gotgbot.FileReader, opts *gotgbot.RequestOpts) (json.RawMessage, error) { // For all sendable methods, we want to allow sending if the message has been deleted. // So, we edit the params to allow for that. // We also log this, for the sake of the example. :) diff --git a/scripts/generate/gen.go b/scripts/generate/gen.go index 15aebb60..9d40c90e 100644 --- a/scripts/generate/gen.go +++ b/scripts/generate/gen.go @@ -218,14 +218,18 @@ const ( tgTypeBoolean = "Boolean" tgTypeFloat = "Float" tgTypeInteger = "Integer" - // These are all custom telegram types. + + // Telegram types which might need special handling. tgTypeMessage = "Message" tgTypeFile = "File" tgTypeInputFile = "InputFile" tgTypeInputMedia = "InputMedia" tgTypeInputPaidMedia = "InputPaidMedia" - // This is actually a custom type. - tgTypeReplyMarkup = "ReplyMarkup" + + // Custom types for this lib. + typeReplyMarkup = "ReplyMarkup" + typeInputFileOrString = "InputFileOrString" + typeInputString = "InputString" ) func generate(d APIDescription) error { @@ -313,13 +317,13 @@ func isTgStructType(d APIDescription, goType string) bool { if !ok { return false } - return len(t.Subtypes) == 0 + return len(t.Fields) != 0 && len(t.Subtypes) == 0 } func (f Field) getPreferredType(d APIDescription) (string, error) { if f.Name == "media" { if len(f.Types) == 1 && f.Types[0] == "String" { - return tgTypeInputFile, nil + return typeInputFileOrString, nil } var arrayType bool mediaType := tgTypeInputMedia @@ -351,7 +355,7 @@ func (f Field) getPreferredType(d APIDescription) (string, error) { // ReplyKeyboardMarkup // ReplyKeyboardRemove // ForceReply - return tgTypeReplyMarkup, nil + return typeReplyMarkup, nil } else if len(f.Types) == 1 { return toGoType(f.Types[0]), nil @@ -396,7 +400,12 @@ func (f Field) getPreferredType(d APIDescription) (string, error) { if len(f.Types) == 2 { if f.Types[0] == tgTypeInputFile && f.Types[1] == tgTypeString { - return toGoType(f.Types[0]), nil + if f.Name == "thumbnail" { + // thumbnails don't support URLs or file_ids; only directly uploaded data: https://t.me/tdlibchat/146804 + return tgTypeInputFile, nil + } + + return typeInputFileOrString, nil } else if f.Types[0] == tgTypeInteger && f.Types[1] == tgTypeString { return toGoType(f.Types[0]), nil } diff --git a/scripts/generate/methods.go b/scripts/generate/methods.go index b0a54493..01aadeea 100644 --- a/scripts/generate/methods.go +++ b/scripts/generate/methods.go @@ -7,8 +7,7 @@ import ( ) var ( - readerBranchTmpl = template.Must(template.New("readerBranch").Parse(readerBranch)) - stringOrReaderBranchTmpl = template.Must(template.New("stringOrReaderBranch").Parse(stringOrReaderBranch)) + inputFileBranchTmpl = template.Must(template.New("inputFileBranch").Parse(inputFileBranch)) inputParamsBranchTmpl = template.Must(template.New("inputParamsBranch").Parse(inputParamsBranch)) inputArrayParamsBranchTmpl = template.Must(template.New("inputArrayParamsBranch").Parse(inputArrayParamsBranch)) ) @@ -22,10 +21,8 @@ func generateMethods(d APIDescription) error { package gotgbot import ( - "bytes" "encoding/json" "fmt" - "io" "strconv" ) `) @@ -229,7 +226,7 @@ func (m MethodDescription) argsToValues(d APIDescription, methodName string, def } if hasData { - return "\ndata := map[string]NamedReader{}" + bd.String(), true, nil + return "\ndata := map[string]FileReader{}" + bd.String(), true, nil } return bd.String(), false, nil @@ -284,7 +281,7 @@ if %s != nil { return "", false, err } - if isPointer(fieldType) || isArray(fieldType) || fieldType == tgTypeReplyMarkup { + if isPointer(fieldType) || isArray(fieldType) || fieldType == typeReplyMarkup { return fmt.Sprintf(` if %s != nil { %s @@ -300,14 +297,8 @@ func stringComplexField(d APIDescription, f Field, fieldType string, goParam str bd := strings.Builder{} // Special case for InputFiles. - if fieldType == tgTypeInputFile { - t := stringOrReaderBranchTmpl - if len(f.Types) == 1 { - // This is actually just an inputfile, not "InputFile or String", so don't support string - t = readerBranchTmpl - } - - err := t.Execute(&bd, readerBranchesData{ + if fieldType == tgTypeInputFile || fieldType == typeInputFileOrString { + err := inputFileBranchTmpl.Execute(&bd, readerBranchesData{ GoParam: goParam, DefaultReturn: defaultRetVal, Name: f.Name, @@ -406,47 +397,14 @@ type readerBranchesData struct { Name string } -const readerBranch = ` -if {{.GoParam}} != nil { - switch m := {{.GoParam}}.(type) { - case NamedReader: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = m - - case io.Reader: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = NamedFile{File: m} - - case []byte: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = NamedFile{File: bytes.NewReader(m)} - - default: - return {{.DefaultReturn}}, fmt.Errorf("unknown type for InputFile: %T",{{.GoParam}}) - } -}` - -const stringOrReaderBranch = ` +// TODO: Make sure this doesn't allow strings, ONLY readers! +const inputFileBranch = ` if {{.GoParam}} != nil { - switch m := {{.GoParam}}.(type) { - case string: - v["{{.Name}}"] = m - - case NamedReader: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = m - - case io.Reader: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = NamedFile{File: m} - - case []byte: - v["{{.Name}}"] = "attach://{{.Name}}" - data["{{.Name}}"] = NamedFile{File: bytes.NewReader(m)} - - default: - return {{.DefaultReturn}}, fmt.Errorf("unknown type for InputFile: %T",{{.GoParam}}) + err := {{.GoParam}}.Attach("{{.Name}}", data) + if err != nil { + return {{.DefaultReturn}}, fmt.Errorf("failed to attach '{{.Name}}' input file: %w", err) } + v["{{.Name}}"] = {{.GoParam}}.getValue() }` const inputParamsBranch = ` diff --git a/scripts/generate/types.go b/scripts/generate/types.go index dba0a0b7..12c90c11 100644 --- a/scripts/generate/types.go +++ b/scripts/generate/types.go @@ -27,12 +27,11 @@ package gotgbot import ( "encoding/json" "fmt" - "io" ) `) // the reply_markup field is weird; this allows it to support multiple types. - replyMarkupInterface, err := generateGenericInterfaceType(d, tgTypeReplyMarkup, getReplyMarkupTypes(d)) + replyMarkupInterface, err := generateGenericInterfaceType(d, typeReplyMarkup, getReplyMarkupTypes(d)) if err != nil { return fmt.Errorf("failed to generate reply_markup interface: %w", err) } @@ -92,9 +91,11 @@ func generateTypeDef(d APIDescription, tgType TypeDescription) (string, error) { return "", fmt.Errorf("failed to check if type requires special handling: %w", err) } if ok { + // TODO: Investigate if thumbnails need special handling too. err = inputParamsTmpl.Execute(&typeDef, inputParamsMethodData{ - Type: tgType.Name, - Field: snakeToTitle(fieldName), + Type: tgType.Name, + Field: snakeToTitle(fieldName), + Thumbnail: containsThumbnail(tgType), }) if err != nil { return "", fmt.Errorf("failed to generate %s inputparam methods: %w", tgType.Name, err) @@ -150,7 +151,7 @@ func containsInputFile(d APIDescription, tgType TypeDescription, checked map[str return false, "", err } - if goType == tgTypeInputFile { + if goType == tgTypeInputFile || goType == typeInputFileOrString || goType == typeInputString { return true, f.Name, nil } @@ -168,6 +169,15 @@ func containsInputFile(d APIDescription, tgType TypeDescription, checked map[str return false, "", nil } +func containsThumbnail(tgType TypeDescription) bool { + for _, f := range tgType.Fields { + if f.Name == "thumbnail" { + return true + } + } + return false +} + func generateParentType(d APIDescription, tgType TypeDescription) (string, error) { subTypes, err := getTypesByName(d, tgType.Subtypes) if err != nil { @@ -281,7 +291,7 @@ func fulfilParentTypeInterfaces(d APIDescription, tgType TypeDescription) (strin for _, t := range getReplyMarkupTypes(d) { if tgType.Name == t.Name { - typeInterfaces.WriteString(generateGenericInterfaceMethod(tgType.Name, tgTypeReplyMarkup)) + typeInterfaces.WriteString(generateGenericInterfaceMethod(tgType.Name, typeReplyMarkup)) break } } @@ -451,6 +461,11 @@ func generateStructFields(d APIDescription, fields []Field, constantFields []str } func generateGenericInterfaceType(d APIDescription, name string, subtypes []TypeDescription) (string, error) { + // We handle inputfiles manually + if name == tgTypeInputFile { + return "", nil + } + if len(subtypes) == 0 { return "\ntype " + name + " interface{}", nil } @@ -481,7 +496,7 @@ func generateGenericInterfaceType(d APIDescription, name string, subtypes []Type } if hasInputFile { bd.WriteString("\n// InputParams allows for uploading attachments with files.") - bd.WriteString("\nInputParams(string, map[string]NamedReader) ([]byte, error)") + bd.WriteString("\nInputParams(string, map[string]FileReader) ([]byte, error)") } if len(commonFields) > 0 && constantField != "" { @@ -747,30 +762,28 @@ func (v {{.Type}}) MarshalJSON() ([]byte, error) { ` type inputParamsMethodData struct { - Type string - Field string + Type string + Field string + Thumbnail bool } const inputParamsMethod = ` -func (v {{.Type}}) InputParams(mediaName string, data map[string]NamedReader) ([]byte, error) { +func (v {{.Type}}) InputParams(mediaName string, data map[string]FileReader) ([]byte, error) { if v.{{.Field}} != nil { - switch m := v.{{.Field}}.(type) { - case string: - // ok, noop - - case NamedReader: - v.{{.Field}} = "attach://" + mediaName - data[mediaName] = m - - case io.Reader: - v.{{.Field}} = "attach://" + mediaName - data[mediaName] = NamedFile{File: m} - - default: - return nil, fmt.Errorf("unknown type: %T", v.{{.Field}}) + err := v.{{.Field}}.Attach(mediaName, data) + if err != nil { + return nil, fmt.Errorf("failed to attach input file for %s: %w", mediaName, err) } } - + {{ if .Thumbnail }} + if v.Thumbnail != nil { + err := v.Thumbnail.Attach(mediaName+"-thumbnail", data) + if err != nil { + return nil, fmt.Errorf("failed to attach 'thumbnail' input file for %s: %w", mediaName, err) + } + } + {{- end }} + return json.Marshal(v) } `