Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,47 @@ bot.SendMediaGroup(ctx, params)

[Demo in examples](examples/send_media_group/main.go)

## InputSticker

For `CreateNewStickerSet` method you can send sticker by file path or file contents.

[Official documentation InputSticker]((https://core.telegram.org/bots/api#inputsticker)

> field `sticker`: 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, or pass “attach://<file_attach_name>” to upload a new file using multipart/form-data under <file_attach_name> name. Animated and video stickers can't be uploaded via HTTP URL.

If you want to use `attach://` format, you should to define `StickerAttachment` field with file content reader.

```go
fileContent, _ := os.ReadFile("/path/to/telegram.png")

inputSticker1 := models.InputSticker{
Sticker: "https://github.com/go-telegram/bot/blob/main/examples/create_new_sticker_set/images/telegram.png?raw=true",
Format: "static",
EmojiList: []string{"1️⃣"},
}

inputSticker2 := models.InputSticker{
Sticker: "attach://telegram.png",
Format: "static",
EmojiList: []string{"2️⃣"},
StickerAttachment: bytes.NewReader(fileContent),
}

params := &bot.CreateNewStickerSetParams{
UserID: update.Message.Chat.ID,
Name: fmt.Sprintf("Example%d_by_%s", time.Now().Unix(), botUsername),
Title: "Example sticker set",
Stickers: []models.InputSticker{
inputSticker1,
inputSticker2,
},
}

b.CreateNewStickerSet(ctx, params)
```

[Demo in examples](examples/create_new_sticker_set/main.go)

## Helpers

### `EscapeMarkdown(s string) string`
Expand Down
31 changes: 31 additions & 0 deletions build_request_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ func buildRequestForm(form *multipart.Writer, params any) (int, error) {
err = addFormFieldInputMediaSlice(form, fieldName, ss)
case []models.InlineQueryResult:
err = addFormFieldInlineQueryResultSlice(form, fieldName, vv)
case []models.InputSticker:
err = addFormFieldInputStickerSlice(form, fieldName, vv)
default:
err = addFormFieldDefault(form, fieldName, v.Field(i).Interface())
}
Expand Down Expand Up @@ -187,6 +189,35 @@ func addFormFieldInlineQueryResultSlice(form *multipart.Writer, fieldName string
return errCopy
}

func addFormFieldInputStickerSlice(form *multipart.Writer, fieldName string, value []models.InputSticker) error {
var lines []string
for _, sticker := range value {
if strings.HasPrefix(sticker.Sticker, "attach://") {
filename := strings.TrimPrefix(sticker.Sticker, "attach://")
attachmentField, errCreateAttachmentField := form.CreateFormFile(filename, filename)
if errCreateAttachmentField != nil {
return errCreateAttachmentField
}
_, errCopy := io.Copy(attachmentField, sticker.StickerAttachment)
if errCopy != nil {
return errCopy
}
}
line, errEncode := json.Marshal(sticker)
if errEncode != nil {
return errEncode
}
lines = append(lines, string(line))
}

w, errCreateField := form.CreateFormField(fieldName)
if errCreateField != nil {
return errCreateField
}
_, errCopy := io.Copy(w, strings.NewReader("["+strings.Join(lines, ",")+"]"))
return errCopy
}

func addFormFieldDefault(form *multipart.Writer, fieldName string, value any) error {
d, errMarshal := json.Marshal(value)
if errMarshal != nil {
Expand Down
31 changes: 27 additions & 4 deletions build_request_form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func Test_buildRequestForm(t *testing.T) {
DefaultInt int `json:"default_int"`
InputMediaInterface models.InputMedia `json:"input_media_interface"`
InlineQueryResultInterface models.InlineQueryResult `json:"inline_query_result_interface"`
InputStickerSlice []models.InputSticker `json:"input_sticker_slice"`
NoJSONTag1 string
NoJSONTag2 string `json:"-"`
OmitEmptyString string `json:"omit_empty_string,omitempty"`
Expand All @@ -46,9 +47,22 @@ func Test_buildRequestForm(t *testing.T) {
DefaultInt: 42,
InputMediaInterface: &models.InputMediaPhoto{Media: "foo", Caption: "bar", ParseMode: "baz"},
InlineQueryResultInterface: &models.InlineQueryResultArticle{Title: "foo", Description: "bar", InputMessageContent: &models.InputTextMessageContent{MessageText: "foo"}},
NoJSONTag1: "foo",
NoJSONTag2: "bar",
OmitEmptyString: "",
InputStickerSlice: []models.InputSticker{
{
Sticker: "attach://sticker.png",
Format: "foo",
EmojiList: []string{"bar"},
StickerAttachment: strings.NewReader("sticker file"),
},
{
Sticker: "foo",
Format: "bar",
EmojiList: []string{"baz"},
},
},
NoJSONTag1: "foo",
NoJSONTag2: "bar",
OmitEmptyString: "",
}

buf := bytes.NewBuffer(nil)
Expand Down Expand Up @@ -102,8 +116,17 @@ Content-Disposition: form-data; name="input_media_interface"
Content-Disposition: form-data; name="inline_query_result_interface"

{"type":"article","id":"","title":"foo","input_message_content":{"message_text":"foo"},"description":"bar"}
--XXX
Content-Disposition: form-data; name="sticker.png"; filename="sticker.png"
Content-Type: application/octet-stream

sticker file
--XXX
Content-Disposition: form-data; name="input_sticker_slice"

[{"sticker":"attach://sticker.png","format":"foo","emoji_list":["bar"]},{"sticker":"foo","format":"bar","emoji_list":["baz"]}]
--XXX--
`
assertEqualInt(t, fieldsCount, 6)
assertEqualInt(t, fieldsCount, 7)
assertFormData(t, buf.String(), expect)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions examples/create_new_sticker_set/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"bytes"
"context"
"embed"
"fmt"
"os"
"os/signal"
"time"

"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
)

// Send any text message to the bot after the bot has been started

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

opts := []bot.Option{
bot.WithDefaultHandler(handler),
bot.WithSkipGetMe(),
}

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)
}

user, err := b.GetMe(ctx)
if nil != err {
// panics for the sake of simplicity.
// you should handle this error properly in your code.
panic(err)
}
botUsername = user.Username
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like bot.Bot can store such info, do not you think so?

Maybe create an issue?
But what to do with skip-get-me option? No get me, no info.


b.Start(ctx)
}

//go:embed images
var images embed.FS
var botUsername string

func handler(ctx context.Context, b *bot.Bot, update *models.Update) {
fileContent, _ := images.ReadFile("images/telegram.png")

inputSticker1 := models.InputSticker{
Sticker: "https://github.com/go-telegram/bot/blob/main/examples/create_new_sticker_set/images/telegram.png?raw=true",
Copy link
Contributor Author

@fregin fregin Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some restrictions for input sticker files, so I could not find any Telegram logo or Golang logo with .PNG extension and with sizes 512x512 (one size can be less)

Such link will be available after merge

Format: "static",
EmojiList: []string{"1️⃣"},
}

inputSticker2 := models.InputSticker{
Sticker: "attach://telegram.png",
Format: "static",
EmojiList: []string{"2️⃣"},
StickerAttachment: bytes.NewReader(fileContent),
}

stickerSetName := fmt.Sprintf("Example%d_by_%s", time.Now().Unix(), botUsername)
params := &bot.CreateNewStickerSetParams{
UserID: update.Message.Chat.ID,
Name: stickerSetName,
Title: "Example sticker set",
Stickers: []models.InputSticker{
inputSticker1,
inputSticker2,
},
}

_, err := b.CreateNewStickerSet(ctx, params)
if nil != err {
// panics for the sake of simplicity.
// you should handle this error properly in your code.
panic(err)
}

stickerSet, err := b.GetStickerSet(ctx, &bot.GetStickerSetParams{Name: stickerSetName})
if err != nil {
// panics for the sake of simplicity.
// you should handle this error properly in your code.
panic(err)
}
_, err = b.SendSticker(
ctx,
&bot.SendStickerParams{
ChatID: update.Message.Chat.ID,
Sticker: &models.InputFileString{
Data: stickerSet.Stickers[0].FileID,
},
},
)
if err != nil {
// panics for the sake of simplicity.
// you should handle this error properly in your code.
panic(err)
}
}
6 changes: 5 additions & 1 deletion models/sticker.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models

import "io"

// MaskPosition https://core.telegram.org/bots/api#maskposition
type MaskPosition struct {
Point string `json:"point"`
Expand Down Expand Up @@ -29,9 +31,11 @@ type Sticker struct {

// InputSticker https://core.telegram.org/bots/api#inputsticker
type InputSticker struct {
Sticker InputFile `json:"sticker"`
Sticker string `json:"sticker"`
Format string `json:"format"`
EmojiList []string `json:"emoji_list"`
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
Keywords []string `json:"keywords,omitempty"`

StickerAttachment io.Reader `json:"-"`
}