diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c2411..66b62ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.6.0 (2024-07-18) + +- support API v7.6, 7.7 +- add const `models.ParseModeMarkdownV1` + ## v1.5.0 (2024-06-24) - support API v7.5 diff --git a/README.md b/README.md index 2f623a6..49aed32 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ > [Telegram Group](https://t.me/gotelegrambotui) -> Supports Bot API version: [7.5](https://core.telegram.org/bots/api#june-18-2024) from June 18, 2024 +> Supports Bot API version: [7.7](https://core.telegram.org/bots/api#july-7-2024) from July 7, 2024 It's a Go zero-dependencies telegram bot framework @@ -343,7 +343,7 @@ b.SendPoll(ctx, p) Return file download link after call method `GetFile` -See [documentation(https://core.telegram.org/bots/api#getfile) +See [documentation](https://core.telegram.org/bots/api#getfile) ## Errors diff --git a/build_request_form.go b/build_request_form.go index 62c7b38..d6fd438 100644 --- a/build_request_form.go +++ b/build_request_form.go @@ -11,12 +11,18 @@ import ( "github.com/go-telegram/bot/models" ) +type inputMedia interface { + MarshalInputMedia() ([]byte, error) + Attachment() io.Reader + GetMedia() string +} + type customMarshal interface { MarshalCustom() ([]byte, error) } var customMarshalInterface = reflect.TypeOf(new(customMarshal)).Elem() -var inputMediaInterface = reflect.TypeOf(new(models.InputMedia)).Elem() +var inputMediaInterface = reflect.TypeOf(new(inputMedia)).Elem() // buildRequestForm builds form-data for request // if params contains InputFile of type InputFileUpload, it will be added to form-data ad upload file. Also, for InputMedia attachments @@ -46,7 +52,7 @@ func buildRequestForm(form *multipart.Writer, params any) (int, error) { continue } if v.Field(i).Type().Implements(inputMediaInterface) { - err := addFormFieldInputMedia(form, fieldName, v.Field(i).Interface().(models.InputMedia)) + err := addFormFieldInputMedia(form, fieldName, v.Field(i).Interface().(inputMedia)) if err != nil { return 0, err } @@ -64,7 +70,17 @@ func buildRequestForm(form *multipart.Writer, params any) (int, error) { case *models.InputFileString: err = addFormFieldString(form, fieldName, vv.Data) case []models.InputMedia: - err = addFormFieldInputMediaSlice(form, fieldName, vv) + var ss []inputMedia + for _, m := range vv { + ss = append(ss, m) + } + err = addFormFieldInputMediaSlice(form, fieldName, ss) + case []models.InputPaidMedia: + var ss []inputMedia + for _, m := range vv { + ss = append(ss, m) + } + err = addFormFieldInputMediaSlice(form, fieldName, ss) case []models.InlineQueryResult: err = addFormFieldInlineQueryResultSlice(form, fieldName, vv) default: @@ -89,7 +105,7 @@ func addFormFieldInputFileUpload(form *multipart.Writer, fieldName string, value return errCopy } -func addFormFieldInputMediaItem(form *multipart.Writer, value models.InputMedia) ([]byte, error) { +func addFormFieldInputMediaItem(form *multipart.Writer, value inputMedia) ([]byte, error) { if strings.HasPrefix(value.GetMedia(), "attach://") { filename := strings.TrimPrefix(value.GetMedia(), "attach://") mediaAttachmentField, errCreateMediaAttachmentField := form.CreateFormFile(filename, filename) @@ -117,7 +133,7 @@ func addFormFieldCustomMarshal(form *multipart.Writer, fieldName string, value c return errCopy } -func addFormFieldInputMedia(form *multipart.Writer, fieldName string, value models.InputMedia) error { +func addFormFieldInputMedia(form *multipart.Writer, fieldName string, value inputMedia) error { line, err := addFormFieldInputMediaItem(form, value) if err != nil { return err @@ -131,7 +147,7 @@ func addFormFieldInputMedia(form *multipart.Writer, fieldName string, value mode return errCopy } -func addFormFieldInputMediaSlice(form *multipart.Writer, fieldName string, value []models.InputMedia) error { +func addFormFieldInputMediaSlice(form *multipart.Writer, fieldName string, value []inputMedia) error { var lines []string for _, media := range value { line, err := addFormFieldInputMediaItem(form, media) diff --git a/examples/send_paid_media/images/facebook.png b/examples/send_paid_media/images/facebook.png new file mode 100644 index 0000000..92a9a62 Binary files /dev/null and b/examples/send_paid_media/images/facebook.png differ diff --git a/examples/send_paid_media/images/youtube.png b/examples/send_paid_media/images/youtube.png new file mode 100644 index 0000000..ad9fc5b Binary files /dev/null and b/examples/send_paid_media/images/youtube.png differ diff --git a/examples/send_paid_media/main.go b/examples/send_paid_media/main.go new file mode 100644 index 0000000..a2d7058 --- /dev/null +++ b/examples/send_paid_media/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "context" + "embed" + "fmt" + "os" + "os/signal" + + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + opts := []bot.Option{ + bot.WithDefaultHandler(handler), + } + + b, err := bot.New(os.Getenv("EXAMPLE_TELEGRAM_BOT_TOKEN"), opts...) + if nil != err { + // panics for the sake of simplicity. + // you should handle this error properly in your code. + panic(err) + } + + b.Start(ctx) +} + +//go:embed images +var images embed.FS + +func handler(ctx context.Context, b *bot.Bot, update *models.Update) { + fileDataFacebook, _ := images.ReadFile("images/facebook.png") + fileDataYoutube, _ := images.ReadFile("images/youtube.png") + + if update.ChannelPost == nil { + fmt.Printf("expect channel post\n") + return + } + + chatID := update.ChannelPost.Chat.ID + + media1 := &models.InputPaidMediaPhoto{ + Media: "https://telegram.org/img/t_logo.png", + } + + media2 := &models.InputPaidMediaPhoto{ + Media: "attach://facebook.png", + MediaAttachment: bytes.NewReader(fileDataFacebook), + } + + media3 := &models.InputPaidMediaPhoto{ + Media: "attach://youtube.png", + MediaAttachment: bytes.NewReader(fileDataYoutube), + } + + params := &bot.SendPaidMediaParams{ + ChatID: chatID, + StarCount: 10, + Media: []models.InputPaidMedia{ + media1, + media2, + media3, + }, + } + + _, err := b.SendPaidMedia(ctx, params) + if err != nil { + fmt.Printf("%+v\n", err) + } +} diff --git a/methods.go b/methods.go index 541de4c..55277b3 100644 --- a/methods.go +++ b/methods.go @@ -132,6 +132,13 @@ func (b *Bot) SendVideoNote(ctx context.Context, params *SendVideoNoteParams) (* return result, err } +// SendPaidMedia https://core.telegram.org/bots/api#sendpaidmedia +func (b *Bot) SendPaidMedia(ctx context.Context, params *SendPaidMediaParams) (*models.Message, error) { + var result models.Message + err := b.rawRequest(ctx, "sendPaidMedia", params, &result) + return &result, err +} + // SendMediaGroup https://core.telegram.org/bots/api#sendmediagroup func (b *Bot) SendMediaGroup(ctx context.Context, params *SendMediaGroupParams) ([]*models.Message, error) { var result []*models.Message diff --git a/methods_params.go b/methods_params.go index da2fbea..d160495 100644 --- a/methods_params.go +++ b/methods_params.go @@ -215,6 +215,21 @@ type SendVideoNoteParams struct { ReplyMarkup models.ReplyMarkup `json:"reply_markup,omitempty"` } +// SendPaidMediaParams https://core.telegram.org/bots/api#sendpaidmedia +type SendPaidMediaParams struct { + ChatID any `json:"chat_id"` + StarCount int `json:"star_count"` + Media []models.InputPaidMedia `json:"media"` + Caption string `json:"caption,omitempty"` + ParseMode models.ParseMode `json:"parse_mode,omitempty"` + CaptionEntities []models.MessageEntity `json:"caption_entities,omitempty"` + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + DisableNotification bool `json:"disable_notification,omitempty"` + ProtectContent bool `json:"protect_content,omitempty"` + ReplyParameters *models.ReplyParameters `json:"reply_parameters,omitempty"` + ReplyMarkup models.ReplyMarkup `json:"reply_markup,omitempty"` +} + // SendMediaGroupParams https://core.telegram.org/bots/api#sendmediagroup type SendMediaGroupParams struct { BusinessConnectionID string `json:"business_connection_id,omitempty"` diff --git a/models/chat.go b/models/chat.go index 029ef5a..4bb6208 100644 --- a/models/chat.go +++ b/models/chat.go @@ -124,6 +124,7 @@ type ChatFullInfo struct { InviteLink string `json:"invite_link,omitempty"` PinnedMessage *Message `json:"pinned_message,omitempty"` Permissions *ChatPermissions `json:"permissions,omitempty"` + CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` SlowModeDelay int `json:"slow_mode_delay,omitempty"` UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"` diff --git a/models/message.go b/models/message.go index f6fffb5..4b7294a 100644 --- a/models/message.go +++ b/models/message.go @@ -99,6 +99,7 @@ type Message struct { Animation *Animation `json:"animation,omitempty"` Audio *Audio `json:"audio,omitempty"` Document *Document `json:"document,omitempty"` + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` Photo []PhotoSize `json:"photo,omitempty"` Sticker *Sticker `json:"sticker,omitempty"` Story *Story `json:"story,omitempty"` @@ -129,6 +130,7 @@ type Message struct { PinnedMessage MaybeInaccessibleMessage `json:"pinned_message,omitempty"` Invoice *Invoice `json:"invoice,omitempty"` SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` + RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"` UsersShared *UsersShared `json:"users_shared,omitempty"` ChatShared *ChatShared `json:"chat_shared,omitempty"` ConnectedWebsite string `json:"connected_website,omitempty"` diff --git a/models/paid.go b/models/paid.go new file mode 100644 index 0000000..e0d5c4c --- /dev/null +++ b/models/paid.go @@ -0,0 +1,150 @@ +package models + +import ( + "encoding/json" + "fmt" + "io" +) + +type InputPaidMedia interface { + inputPaidMediaTag() + + MarshalInputMedia() ([]byte, error) + Attachment() io.Reader + GetMedia() string +} + +// InputPaidMediaPhoto https://core.telegram.org/bots/api#inputpaidmediaphoto +type InputPaidMediaPhoto struct { + Media string `json:"media"` + + MediaAttachment io.Reader `json:"-"` +} + +func (m *InputPaidMediaPhoto) inputPaidMediaTag() {} + +func (m *InputPaidMediaPhoto) Attachment() io.Reader { + return m.MediaAttachment +} + +func (m *InputPaidMediaPhoto) GetMedia() string { + return m.Media +} + +func (m *InputPaidMediaPhoto) MarshalInputMedia() ([]byte, error) { + ret := struct { + Type string `json:"type"` + *InputPaidMediaPhoto + }{ + Type: "photo", + InputPaidMediaPhoto: m, + } + + return json.Marshal(&ret) +} + +// InputPaidMediaVideo https://core.telegram.org/bots/api#inputpaidmediavideo +type InputPaidMediaVideo struct { + Media string `json:"media"` + Thumbnail InputFile `json:"thumbnail,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Duration int `json:"duration,omitempty"` + SupportsStreaming bool `json:"supports_streaming,omitempty"` + + MediaAttachment io.Reader `json:"-"` +} + +func (m *InputPaidMediaVideo) inputPaidMediaTag() {} + +func (m *InputPaidMediaVideo) Attachment() io.Reader { + return m.MediaAttachment +} + +func (m *InputPaidMediaVideo) GetMedia() string { + return m.Media +} + +func (m *InputPaidMediaVideo) MarshalInputMedia() ([]byte, error) { + ret := struct { + Type string `json:"type"` + *InputPaidMediaVideo + }{ + Type: "video", + InputPaidMediaVideo: m, + } + + return json.Marshal(&ret) +} + +type PaidMediaType string + +const ( + PaidMediaTypePreview PaidMediaType = "preview" + PaidMediaTypePhoto PaidMediaType = "photo" + PaidMediaTypeVideo PaidMediaType = "video" +) + +// PaidMedia https://core.telegram.org/bots/api#paidmedia +type PaidMedia struct { + Type PaidMediaType + + Preview *PaidMediaPreview + Photo *PaidMediaPhoto + Video *PaidMediaVideo +} + +func (p *PaidMedia) UnmarshalJSON(data []byte) error { + v := struct { + Type PaidMediaType `json:"type"` + }{} + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + + p.Type = v.Type + + switch v.Type { + case PaidMediaTypePreview: + p.Preview = &PaidMediaPreview{} + return json.Unmarshal(data, p.Preview) + case PaidMediaTypePhoto: + p.Photo = &PaidMediaPhoto{} + return json.Unmarshal(data, p.Photo) + case PaidMediaTypeVideo: + p.Video = &PaidMediaVideo{} + return json.Unmarshal(data, p.Video) + default: + return fmt.Errorf("unsupported PaidMedia type, %v", v.Type) + } +} + +// PaidMediaPreview https://core.telegram.org/bots/api#paidmediapreview +type PaidMediaPreview struct { + Type PaidMediaType + + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Duration int `json:"duration,omitempty"` +} + +// PaidMediaPhoto https://core.telegram.org/bots/api#paidmediaphoto +type PaidMediaPhoto struct { + Type PaidMediaType + + Photo []PhotoSize `json:"photo"` +} + +// PaidMediaVideo https://core.telegram.org/bots/api#paidmediavideo +type PaidMediaVideo struct { + Type PaidMediaType + + Video Video `json:"video"` +} + +// PaidMediaInfo https://core.telegram.org/bots/api#paidmediainfo +type PaidMediaInfo struct { + StarCount int `json:"star_count"` + PaidMedia []PaidMedia `json:"paid_media"` +} diff --git a/models/paid_test.go b/models/paid_test.go new file mode 100644 index 0000000..775a2ea --- /dev/null +++ b/models/paid_test.go @@ -0,0 +1,90 @@ +package models + +import "testing" + +func TestPaidMedia_UnmarshalJSON_Preview(t *testing.T) { + src := `{"type":"preview","width":42,"height":43,"duration":44}` + + p := &PaidMedia{} + err := p.UnmarshalJSON([]byte(src)) + if err != nil { + t.Fatal(err) + } + + if p.Type != PaidMediaTypePreview { + t.Fatal("wrong type") + } + + if p.Preview == nil { + t.Fatal("Preview is nil") + } + + if p.Preview.Width != 42 { + t.Fatal("wrong width") + } + + if p.Preview.Height != 43 { + t.Fatal("wrong height") + } + + if p.Preview.Duration != 44 { + t.Fatal("wrong duration") + } +} + +func TestPaidMedia_UnmarshalJSON_Photo(t *testing.T) { + src := `{"type":"photo","photo":[{"width":42,"height":43}]}` + + p := &PaidMedia{} + err := p.UnmarshalJSON([]byte(src)) + if err != nil { + t.Fatal(err) + } + + if p.Type != PaidMediaTypePhoto { + t.Fatal("wrong type") + } + + if p.Photo == nil { + t.Fatal("Photo is nil") + } + + if len(p.Photo.Photo) != 1 { + t.Fatal("wrong photo length") + } + + if p.Photo.Photo[0].Width != 42 { + t.Fatal("wrong width") + } + if p.Photo.Photo[0].Height != 43 { + t.Fatal("wrong height") + } +} + +func TestPaidMedia_UnmarshalJSON_Video(t *testing.T) { + src := `{"type":"video","video":{"width":42,"height":43,"duration":44}}` + + p := &PaidMedia{} + err := p.UnmarshalJSON([]byte(src)) + if err != nil { + t.Fatal(err) + } + + if p.Type != PaidMediaTypeVideo { + t.Fatal("wrong type") + } + + if p.Video == nil { + t.Fatal("Video is nil") + } + + if p.Video.Video.Width != 42 { + t.Fatal("wrong width") + } + if p.Video.Video.Height != 43 { + t.Fatal("wrong height") + } + if p.Video.Video.Duration != 44 { + t.Fatal("wrong duration") + } +} diff --git a/models/parse_mode.go b/models/parse_mode.go index d444686..ddc664c 100644 --- a/models/parse_mode.go +++ b/models/parse_mode.go @@ -3,6 +3,7 @@ package models type ParseMode string const ( - ParseModeMarkdown ParseMode = "MarkdownV2" - ParseModeHTML ParseMode = "HTML" + ParseModeMarkdownV1 ParseMode = "Markdown" + ParseModeMarkdown ParseMode = "MarkdownV2" + ParseModeHTML ParseMode = "HTML" ) diff --git a/models/reply.go b/models/reply.go index 718c901..32ff250 100644 --- a/models/reply.go +++ b/models/reply.go @@ -22,6 +22,7 @@ type ExternalReplyInfo struct { Animation *Animation `json:"animation,omitempty"` Audio *Audio `json:"audio,omitempty"` Document *Document `json:"document,omitempty"` + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` Photo []PhotoSize `json:"photo,omitempty"` Sticker *Sticker `json:"sticker,omitempty"` Story *Story `json:"story,omitempty"` diff --git a/models/star.go b/models/star.go index e557182..721eac0 100644 --- a/models/star.go +++ b/models/star.go @@ -57,8 +57,9 @@ type TransactionPartnerFragment struct { // TransactionPartnerUser https://core.telegram.org/bots/api#transactionpartneruser type TransactionPartnerUser struct { - Type TransactionPartnerType `json:"type"` - User User `json:"user"` + Type TransactionPartnerType `json:"type"` + User User `json:"user"` + InvoicePayload string `json:"invoice_payload,omitempty"` } // TransactionPartnerOther https://core.telegram.org/bots/api#transactionpartnerother @@ -140,3 +141,8 @@ type StarTransaction struct { type StarTransactions struct { Transactions []StarTransaction `json:"transactions"` } + +// TransactionPartnerTelegramAds https://core.telegram.org/bots/api#transactionpartnertelegramads +type TransactionPartnerTelegramAds struct { + Type string `json:"type"` +} diff --git a/models/successful_payment.go b/models/successful_payment.go index 73b8839..e35573f 100644 --- a/models/successful_payment.go +++ b/models/successful_payment.go @@ -10,3 +10,12 @@ type SuccessfulPayment struct { TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` ProviderPaymentChargeID string `json:"provider_payment_charge_id"` } + +// RefundedPayment https://core.telegram.org/bots/api#refundedpayment +type RefundedPayment struct { + Currency string `json:"currency"` + TotalAmount int `json:"total_amount"` + InvoicePayload string `json:"invoice_payload"` + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + ProviderPaymentChargeID string `json:"provider_payment_charge_id,omitempty"` +}