release: 1.0.0 beta 22

Implemented full tgapi method coverage from Telegram docs, aligned numeric ID/file_size types, and fixed method signatures/JSON tags.; Standardized GoDoc across exported APIs with Telegram links and refreshed README sections for MsgContext plus API/Uploader usage.
This commit is contained in:
2026-03-17 13:21:06 +03:00
parent 389ec9f9d7
commit 2a3607bdea
48 changed files with 921 additions and 284 deletions

View File

@@ -129,13 +129,22 @@ Provides access to the incoming message and useful reply methods:
- `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message with parse_mode none and inline keyboard. - `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message with parse_mode none and inline keyboard.
- `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping) and inline keyboard. - `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping) and inline keyboard.
- `AnswerPhoto(photoId, text string) *AnswerMessage`: Sends a message with photo with parse_mode none. - `AnswerPhoto(photoId, text string) *AnswerMessage`: Sends a message with photo with parse_mode none.
- `AnswerPhotoMarkdown(photoId, text string) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping) with. - `AnswerPhotoMarkdown(photoId, text string) *AnswerMessage`: Sends a photo with MarkdownV2 caption (you handle escaping).
- `EditCallback(text string)`: Edits message with parse_mode none after clicking inline button. - `EditCallback(text string)`: Edits message with parse_mode none after clicking inline button.
- `EditCallbackMarkdown(text string)`: Edits a message formatted with MarkdownV2 (you handle escaping) after clicking inline button. - `EditCallbackMarkdown(text string)`: Edits a message formatted with MarkdownV2 (you handle escaping) after clicking inline button.
- `SendChatAction(action string)`: Sends a “typing”, “uploading photo”, etc., action. - `SendAction(action tgapi.ChatActionType)`: Sends a “typing”, “uploading photo”, etc., action.
- Fields: `Text`, `Args`, `From`, `Chat`, `Msg`, etc. - Fields: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgId`, `CallbackQueryId`, etc.
- And more methods and fields! - And more methods and fields!
### tgapi: API and Uploader
`tgapi` provides two clients:
- `API` for JSON requests (e.g., `SendMessage`, `EditMessageText`, methods using file_id/URL).
- `Uploader` for multipart uploads (e.g., `SendPhoto`, `SendDocument`, `SendVideo` with binary files).
This split keeps method intent explicit: JSON-only calls go through `API`, file uploads go through `Uploader`.
### Database Context ### Database Context
The `T` in `NewBot[T]` is a powerful feature. You can pass any type (like a database connection pool), and it will be available in every command and middleware handler. The `T` in `NewBot[T]` is a powerful feature. You can pass any type (like a database connection pool), and it will be available in every command and middleware handler.
@@ -193,7 +202,7 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
- Middleware can modify the MsgContext (e.g., add custom fields) before the command runs. - Middleware can modify the MsgContext (e.g., add custom fields) before the command runs.
## ⚙️ Advanced Configuration ## ⚙️ Advanced Configuration
- **Inline Keyboards**: Build keyboards using laniakea.NewKeyboard(). - **Inline Keyboards**: Build keyboards using `laniakea.NewInlineKeyboardJson`, `laniakea.NewInlineKeyboardBase64`, or `laniakea.NewInlineKeyboard`.
- **Rate Limiting**: Pass a configured utils.RateLimiter via BotOpts to handle Telegram's rate limits gracefully. - **Rate Limiting**: Pass a configured utils.RateLimiter via BotOpts to handle Telegram's rate limits gracefully.
- **Custom HTTP Client**: Provide your own http.Client in BotOpts for fine-tuned control. - **Custom HTTP Client**: Provide your own http.Client in BotOpts for fine-tuned control.

25
bot.go
View File

@@ -81,6 +81,8 @@ type Bot[T DbContext] struct {
updateOffset int // Last processed update ID updateOffset int // Last processed update ID
updateTypes []tgapi.UpdateType // Types of updates to fetch updateTypes []tgapi.UpdateType // Types of updates to fetch
updateQueue chan *tgapi.Update // Internal queue for processing updates updateQueue chan *tgapi.Update // Internal queue for processing updates
runnerOnceWG sync.WaitGroup // Tracks one-time async runners
runnerBgWG sync.WaitGroup // Tracks background async runners
} }
// NewBot creates and initializes a new Bot instance using the provided BotOpts. // NewBot creates and initializes a new Bot instance using the provided BotOpts.
@@ -107,11 +109,13 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
// limiter = utils.NewRateLimiter() // limiter = utils.NewRateLimiter()
//} //}
limiter := utils.NewRateLimiter() limiter := utils.NewRateLimiter()
limiter.SetGlobalRate(opts.RateLimit)
apiOpts := tgapi.NewAPIOpts(opts.Token). apiOpts := tgapi.NewAPIOpts(opts.Token).
SetAPIUrl(opts.APIUrl). SetAPIUrl(opts.APIUrl).
UseTestServer(opts.UseTestServer). UseTestServer(opts.UseTestServer).
SetLimiter(limiter) SetLimiter(limiter).
SetLimiterDrop(opts.DropRLOverflow)
api := tgapi.NewAPI(apiOpts) api := tgapi.NewAPI(apiOpts)
uploader := tgapi.NewUploader(api) uploader := tgapi.NewUploader(api)
@@ -137,7 +141,7 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
prefixes: prefixes, prefixes: prefixes,
token: opts.Token, token: opts.Token,
plugins: make([]Plugin[T], 0), plugins: make([]Plugin[T], 0),
updateTypes: make([]tgapi.UpdateType, 0), updateTypes: append([]tgapi.UpdateType{}, opts.UpdateTypes...),
runners: make([]Runner[T], 0), runners: make([]Runner[T], 0),
extraLoggers: make([]*slog.Logger, 0), extraLoggers: make([]*slog.Logger, 0),
l10n: &L10n{}, l10n: &L10n{},
@@ -285,9 +289,9 @@ func (bot *Bot[T]) UpdateTypes(t ...tgapi.UpdateType) *Bot[T] {
return bot return bot
} }
// SetPayloadType sets the type, that bot will use for payload // SetPayloadType sets the payload encoding type used for callback data.
// json - string `{"cmd": "command", "args": [...]} // JSON stores payload as a string: `{"cmd":"command","args":[...]}`.
// base64 - same json, but encoded in base64 string // Base64 stores the same JSON encoded as a Base64URL string.
func (bot *Bot[T]) SetPayloadType(t BotPayloadType) *Bot[T] { func (bot *Bot[T]) SetPayloadType(t BotPayloadType) *Bot[T] {
bot.payloadType = t bot.payloadType = t
return bot return bot
@@ -309,7 +313,7 @@ func (bot *Bot[T]) AddPrefixes(prefixes ...string) *Bot[T] {
// ErrorTemplate sets the format string for error messages sent to users. // ErrorTemplate sets the format string for error messages sent to users.
// Use "%s" to insert the error message. // Use "%s" to insert the error message.
// Example: "❌ Error: %s" → "❌ Error: Command not found" // Example: "❌ Error: %s" → "❌ Error: Command not found".
func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] { func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] {
bot.errorTemplate = s bot.errorTemplate = s
return bot return bot
@@ -408,6 +412,7 @@ func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] {
func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] { func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] {
if l == nil { if l == nil {
bot.logger.Warn("AddL10n called with nil L10n; localization will be disabled") bot.logger.Warn("AddL10n called with nil L10n; localization will be disabled")
return bot
} }
bot.l10n = l bot.l10n = l
return bot return bot
@@ -457,6 +462,12 @@ func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
// // ... later ... // // ... later ...
// cancel() // triggers graceful shutdown // cancel() // triggers graceful shutdown
func (bot *Bot[T]) RunWithContext(ctx context.Context) { func (bot *Bot[T]) RunWithContext(ctx context.Context) {
defer func() {
if err := bot.Close(); err != nil {
bot.logger.Errorln(err)
}
}()
if len(bot.prefixes) == 0 { if len(bot.prefixes) == 0 {
bot.logger.Fatalln("no prefixes defined") bot.logger.Fatalln("no prefixes defined")
return return
@@ -512,6 +523,8 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) {
}) })
} }
pool.Stop() // Wait for all tasks to complete and stop the pool pool.Stop() // Wait for all tasks to complete and stop the pool
bot.runnerOnceWG.Wait()
bot.runnerBgWG.Wait()
} }
// Run starts the bot using a background context. // Run starts the bot using a background context.

View File

@@ -119,7 +119,7 @@ func (opts *BotOpts) SetToken(token string) *BotOpts {
// SetUpdateTypes sets the list of update types to listen for. // SetUpdateTypes sets the list of update types to listen for.
// If empty (default), Telegram will return all update types. // If empty (default), Telegram will return all update types.
// Example: opts.SetUpdateTypes("message", "callback_query") // Example: opts.SetUpdateTypes("message", "callback_query").
func (opts *BotOpts) SetUpdateTypes(types ...tgapi.UpdateType) *BotOpts { func (opts *BotOpts) SetUpdateTypes(types ...tgapi.UpdateType) *BotOpts {
opts.UpdateTypes = types opts.UpdateTypes = types
return opts return opts

View File

@@ -9,6 +9,7 @@ import (
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.nix13.pw/scuroneko/laniakea/tgapi"
) )
// CmdRegexp matches command names allowed for Telegram command registration.
var CmdRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$") var CmdRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$")
// ErrTooManyCommands is returned when the total number of registered commands // ErrTooManyCommands is returned when the total number of registered commands
@@ -19,21 +20,7 @@ var CmdRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$")
// bot initialization. // bot initialization.
var ErrTooManyCommands = errors.New("too many commands. max 100") var ErrTooManyCommands = errors.New("too many commands. max 100")
// generateBotCommand converts a Command[T] into a tgapi.BotCommand with a // generateBotCommand builds a BotCommand description with generated usage text.
// formatted description that includes usage instructions.
//
// The description is built as:
//
// "<original_description>. Usage: /<command> <arg1> [<arg2>] ..."
//
// Required arguments are shown as-is; optional arguments are wrapped in square brackets.
//
// Example:
//
// Command{command: "start", description: "Start the bot", args: []Arg{{text: "name", required: false}}}
// → Description: "Start the bot. Usage: /start [name]"
// Command{command: "echo", description: "Echo user input", args: []Arg{{text: "name", required: true}}}
// → Description: "Echo user input. Usage: /echo <input>"
func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand { func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
desc := "" desc := ""
if len(cmd.description) > 0 { if len(cmd.description) > 0 {
@@ -57,16 +44,10 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
return tgapi.BotCommand{Command: cmd.command, Description: usage} return tgapi.BotCommand{Command: cmd.command, Description: usage}
} }
// checkCmdRegex check if command satisfy regexp [a-zA-Z0-9]+ // checkCmdRegex reports whether cmd matches CmdRegexp.
// Return true if satisfied, else false.
func checkCmdRegex(cmd string) bool { return CmdRegexp.MatchString(cmd) } func checkCmdRegex(cmd string) bool { return CmdRegexp.MatchString(cmd) }
// gatherCommandsForPlugin collects all non-skipped commands from a Plugin[T] // gatherCommandsForPlugin collects non-skipped, valid commands from one plugin.
// and converts them into tgapi.BotCommand objects.
//
// Commands marked with skipAutoCmd = true are excluded from auto-registration.
// This allows plugins to opt out of automatic command generation (e.g., for
// internal or hidden commands).
func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand { func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0) commands := make([]tgapi.BotCommand, 0)
for _, cmd := range pl.commands { for _, cmd := range pl.commands {
@@ -83,7 +64,7 @@ func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
// gatherCommands collects all commands from all plugins // gatherCommands collects all commands from all plugins
// and converts them into tgapi.BotCommand objects. // and converts them into tgapi.BotCommand objects.
// See gatherCommandsForPlugin // See gatherCommandsForPlugin.
func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand { func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0) commands := make([]tgapi.BotCommand, 0)
for _, pl := range bot.plugins { for _, pl := range bot.plugins {

View File

@@ -9,6 +9,7 @@ import (
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.nix13.pw/scuroneko/laniakea/tgapi"
) )
// ErrDraftChatIDZero is returned when a draft is used without setting a chat ID.
var ErrDraftChatIDZero = errors.New("zero draft chat ID") var ErrDraftChatIDZero = errors.New("zero draft chat ID")
// draftIdGenerator defines an interface for generating unique draft IDs. // draftIdGenerator defines an interface for generating unique draft IDs.
@@ -90,27 +91,25 @@ func (p *DraftProvider) GetDraft(id uint64) (*Draft, bool) {
// FlushAll sends all pending drafts as final messages and clears them. // FlushAll sends all pending drafts as final messages and clears them.
// //
// If any draft fails to send, FlushAll returns the error immediately and // If one or more drafts fail to send, FlushAll still attempts all drafts and
// leaves other drafts unflushed. This allows for retry logic or logging. // returns the first encountered error.
// //
// After successful flush, each draft is removed from the provider and cleared. // After successful flush, each draft is removed from the provider and cleared.
func (p *DraftProvider) FlushAll() error { func (p *DraftProvider) FlushAll() error {
p.mu.Lock() p.mu.RLock()
drafts := make([]*Draft, 0, len(p.drafts)) drafts := make([]*Draft, 0, len(p.drafts))
for _, draft := range p.drafts { for _, draft := range p.drafts {
drafts = append(drafts, draft) drafts = append(drafts, draft)
} }
p.drafts = make(map[uint64]*Draft) p.mu.RUnlock()
p.mu.Unlock()
var lastErr error var firstErr error
for _, draft := range drafts { for _, draft := range drafts {
if err := draft.Flush(); err != nil { if err := draft.Flush(); err != nil && firstErr == nil {
lastErr = err firstErr = err
break // Stop on first error to avoid partial state
} }
} }
return lastErr return firstErr
} }
// Draft represents a single message draft that can be edited and flushed. // Draft represents a single message draft that can be edited and flushed.
@@ -165,7 +164,7 @@ func (d *Draft) SetChat(chatID int64, messageThreadID int) *Draft {
// SetEntities replaces the draft's message entities. // SetEntities replaces the draft's message entities.
// //
// Entities are stored by reference. If you plan to mutate the slice later, // Entities are stored by reference. If you plan to mutate the slice later,
// pass a copy: `SetEntities(append([]tgapi.MessageEntity{}, myEntities...))` // pass a copy: `SetEntities(append([]tgapi.MessageEntity{}, myEntities...))`.
func (d *Draft) SetEntities(entities []tgapi.MessageEntity) *Draft { func (d *Draft) SetEntities(entities []tgapi.MessageEntity) *Draft {
d.entities = entities d.entities = entities
return d return d

View File

@@ -10,6 +10,7 @@ import (
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.nix13.pw/scuroneko/laniakea/tgapi"
) )
// ErrInvalidPayloadType is returned when callback payload encoding type is unknown.
var ErrInvalidPayloadType = errors.New("invalid payload type") var ErrInvalidPayloadType = errors.New("invalid payload type")
func (bot *Bot[T]) handle(u *tgapi.Update) { func (bot *Bot[T]) handle(u *tgapi.Update) {
@@ -28,7 +29,9 @@ func (bot *Bot[T]) handle(u *tgapi.Update) {
payloadType: bot.payloadType, payloadType: bot.payloadType,
} }
for _, middleware := range bot.middlewares { for _, middleware := range bot.middlewares {
middleware.Execute(ctx, bot.dbContext) if !middleware.Execute(ctx, bot.dbContext) {
return
}
} }
if u.CallbackQuery != nil { if u.CallbackQuery != nil {
@@ -42,6 +45,9 @@ func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) {
if update.Message == nil { if update.Message == nil {
return return
} }
if update.Message.From == nil {
return
}
var text string var text string
if len(update.Message.Text) > 0 { if len(update.Message.Text) > 0 {
@@ -106,8 +112,13 @@ func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) {
ctx.FromID = update.CallbackQuery.From.ID ctx.FromID = update.CallbackQuery.From.ID
ctx.From = &update.CallbackQuery.From ctx.From = &update.CallbackQuery.From
ctx.Msg = &update.CallbackQuery.Message if update.CallbackQuery.Message != nil {
ctx.Msg = update.CallbackQuery.Message
ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID
}
if update.CallbackQuery.InlineMessageID != nil {
ctx.InlineMsgId = *update.CallbackQuery.InlineMessageID
}
ctx.CallbackQueryId = update.CallbackQuery.ID ctx.CallbackQueryId = update.CallbackQuery.ID
ctx.Args = data.Args ctx.Args = data.Args

View File

@@ -69,7 +69,7 @@ func (b InlineKbButtonBuilder) SetUrl(url string) InlineKbButtonBuilder {
// Args are converted to strings using fmt.Sprint. Non-string types (e.g., int, bool) // Args are converted to strings using fmt.Sprint. Non-string types (e.g., int, bool)
// are safely serialized, but complex structs may not serialize usefully. // are safely serialized, but complex structs may not serialize usefully.
// //
// Example: SetCallbackDataJson("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]} // Example: SetCallbackDataJson("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]}.
func (b InlineKbButtonBuilder) SetCallbackDataJson(cmd string, args ...any) InlineKbButtonBuilder { func (b InlineKbButtonBuilder) SetCallbackDataJson(cmd string, args ...any) InlineKbButtonBuilder {
b.callbackData = NewCallbackData(cmd, args...).ToJson() b.callbackData = NewCallbackData(cmd, args...).ToJson()
return b return b
@@ -210,7 +210,7 @@ func (in *InlineKeyboard) AddLine() *InlineKeyboard {
// Returns a pointer to a ReplyMarkup suitable for use with tgapi.SendMessage. // Returns a pointer to a ReplyMarkup suitable for use with tgapi.SendMessage.
func (in *InlineKeyboard) Get() *tgapi.ReplyMarkup { func (in *InlineKeyboard) Get() *tgapi.ReplyMarkup {
if in.CurrentLine.Len() > 0 { if in.CurrentLine.Len() > 0 {
in.Lines = append(in.Lines, in.CurrentLine) in.AddLine()
} }
return &tgapi.ReplyMarkup{InlineKeyboard: in.Lines} return &tgapi.ReplyMarkup{InlineKeyboard: in.Lines}
} }

View File

@@ -1,7 +1,7 @@
package laniakea package laniakea
// DictEntry represents a single localized entry with language-to-text mappings. // DictEntry represents a single localized entry with language-to-text mappings.
// Example: {"ru": "Привет", "en": "Hello"} // Example: {"ru": "Привет", "en": "Hello"}.
type DictEntry map[string]string type DictEntry map[string]string
// L10n is a localization manager that maps keys to language-specific strings. // L10n is a localization manager that maps keys to language-specific strings.

View File

@@ -19,9 +19,10 @@ type MsgContext struct {
Msg *tgapi.Message Msg *tgapi.Message
From *tgapi.User From *tgapi.User
InlineMsgId string
CallbackMsgId int CallbackMsgId int
CallbackQueryId string CallbackQueryId string
FromID int FromID int64
Prefix string Prefix string
Text string Text string
Args []string Args []string
@@ -46,11 +47,19 @@ type AnswerMessage struct {
// Used by Edit, EditMarkdown, EditCallback, etc. // Used by Edit, EditMarkdown, EditCallback, etc.
func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
params := tgapi.EditMessageTextP{ params := tgapi.EditMessageTextP{
MessageID: messageId,
ChatID: ctx.Msg.Chat.ID,
Text: text, Text: text,
ParseMode: parseMode, ParseMode: parseMode,
} }
switch {
case messageId > 0 && ctx.Msg != nil:
params.MessageID = messageId
params.ChatID = ctx.Msg.Chat.ID
case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId
default:
ctx.botLogger.Errorln("Can't edit message: no valid message target")
return nil
}
if keyboard != nil { if keyboard != nil {
params.ReplyMarkup = keyboard.Get() params.ReplyMarkup = keyboard.Get()
} }
@@ -59,8 +68,12 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard
ctx.botLogger.Errorln(err) ctx.botLogger.Errorln(err)
return nil return nil
} }
resultMessageID := messageId
if msg.MessageID > 0 {
resultMessageID = msg.MessageID
}
return &AnswerMessage{ return &AnswerMessage{
MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: false, MessageID: resultMessageID, ctx: ctx, Text: text, IsMedia: false,
} }
} }
@@ -79,9 +92,9 @@ func (m *AnswerMessage) EditMarkdown(text string) *AnswerMessage {
} }
// editCallback is an internal helper to edit the message associated with a callback query. // editCallback is an internal helper to edit the message associated with a callback query.
// Returns nil if CallbackMsgId is 0 (not a callback context). // Supports both regular callback messages and inline callback messages.
func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.CallbackMsgId == 0 { if ctx.CallbackMsgId == 0 && ctx.InlineMsgId == "" {
ctx.botLogger.Errorln("Can't edit non-callback update message") ctx.botLogger.Errorln("Can't edit non-callback update message")
return nil return nil
} }
@@ -113,18 +126,22 @@ func (ctx *MsgContext) EditCallbackfMarkdown(format string, keyboard *InlineKeyb
} }
// editPhotoText edits the caption of a photo/video message. // editPhotoText edits the caption of a photo/video message.
// Returns nil if messageId is 0. // Returns nil when no valid edit target is available for the current context.
func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if messageId == 0 {
ctx.botLogger.Errorln("Can't edit caption message, message ID zero")
return nil
}
params := tgapi.EditMessageCaptionP{ params := tgapi.EditMessageCaptionP{
ChatID: ctx.Msg.Chat.ID,
MessageID: messageId,
Caption: text, Caption: text,
ParseMode: parseMode, ParseMode: parseMode,
} }
switch {
case messageId > 0 && ctx.Msg != nil:
params.ChatID = ctx.Msg.Chat.ID
params.MessageID = messageId
case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId
default:
ctx.botLogger.Errorln("Can't edit caption: no valid message target")
return nil
}
if kb != nil { if kb != nil {
params.ReplyMarkup = kb.Get() params.ReplyMarkup = kb.Get()
} }
@@ -132,9 +149,14 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
msg, _, err := ctx.Api.EditMessageCaption(params) msg, _, err := ctx.Api.EditMessageCaption(params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.botLogger.Errorln(err)
return nil
}
resultMessageID := messageId
if msg.MessageID > 0 {
resultMessageID = msg.MessageID
} }
return &AnswerMessage{ return &AnswerMessage{
MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, MessageID: resultMessageID, ctx: ctx, Text: text, IsMedia: true,
} }
} }
@@ -165,6 +187,10 @@ func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeybo
// answer sends a new message with optional keyboard and parse mode. // answer sends a new message with optional keyboard and parse mode.
// Uses API limiter to respect Telegram rate limits per chat. // Uses API limiter to respect Telegram rate limits per chat.
func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't answer message without a message")
return nil
}
params := tgapi.SendMessageP{ params := tgapi.SendMessageP{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
Text: text, Text: text,
@@ -180,11 +206,6 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode t
params.DirectMessagesTopicID = ctx.Msg.DirectMessageTopic.TopicID params.DirectMessagesTopicID = ctx.Msg.DirectMessageTopic.TopicID
} }
cont := context.Background()
if err := ctx.Api.Limiter.Wait(cont, ctx.Msg.Chat.ID); err != nil {
ctx.botLogger.Errorln(err)
return nil
}
msg, err := ctx.Api.SendMessage(params) msg, err := ctx.Api.SendMessage(params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.botLogger.Errorln(err)
@@ -233,6 +254,10 @@ func (ctx *MsgContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *
// answerPhoto sends a photo with optional caption and keyboard. // answerPhoto sends a photo with optional caption and keyboard.
func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't answer message without a message")
return nil
}
params := tgapi.SendPhotoP{ params := tgapi.SendPhotoP{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
Caption: text, Caption: text,
@@ -294,6 +319,14 @@ func (ctx *MsgContext) AnswerPhotofMarkdown(photoId, template string, args ...an
// delete removes a message by ID. // delete removes a message by ID.
func (ctx *MsgContext) delete(messageId int) { func (ctx *MsgContext) delete(messageId int) {
if messageId == 0 {
ctx.botLogger.Errorln("Can't delete message: message ID zero")
return
}
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't delete message: no chat message context")
return
}
_, err := ctx.Api.DeleteMessage(tgapi.DeleteMessageP{ _, err := ctx.Api.DeleteMessage(tgapi.DeleteMessageP{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
MessageID: messageId, MessageID: messageId,
@@ -307,7 +340,13 @@ func (ctx *MsgContext) delete(messageId int) {
func (m *AnswerMessage) Delete() { m.ctx.delete(m.MessageID) } func (m *AnswerMessage) Delete() { m.ctx.delete(m.MessageID) }
// CallbackDelete deletes the message that triggered the callback query. // CallbackDelete deletes the message that triggered the callback query.
func (ctx *MsgContext) CallbackDelete() { ctx.delete(ctx.CallbackMsgId) } func (ctx *MsgContext) CallbackDelete() {
if ctx.CallbackMsgId == 0 {
ctx.botLogger.Errorln("Can't delete callback message: no callback message ID")
return
}
ctx.delete(ctx.CallbackMsgId)
}
// answerCallbackQuery sends a response to a callback query (optional text/alert/url). // answerCallbackQuery sends a response to a callback query (optional text/alert/url).
// Does nothing if CallbackQueryId is empty. // Does nothing if CallbackQueryId is empty.
@@ -338,6 +377,10 @@ func (ctx *MsgContext) AnswerCbQueryUrl(u string) { ctx.answerCallbackQuery(u, "
// SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity. // SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity.
func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) { func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't send action without chat message context")
return
}
params := tgapi.SendChatActionP{ params := tgapi.SendChatActionP{
ChatID: ctx.Msg.Chat.ID, Action: action, ChatID: ctx.Msg.Chat.ID, Action: action,
} }

View File

@@ -26,7 +26,7 @@ var (
CommandRegexInt = regexp.MustCompile(`\d+`) CommandRegexInt = regexp.MustCompile(`\d+`)
// CommandRegexString matches any non-empty string. // CommandRegexString matches any non-empty string.
CommandRegexString = regexp.MustCompile(`.+`) CommandRegexString = regexp.MustCompile(`.+`)
// CommandRegexBool matches true or false // CommandRegexBool matches true or false.
CommandRegexBool = regexp.MustCompile(`true|false`) CommandRegexBool = regexp.MustCompile(`true|false`)
) )
@@ -53,6 +53,7 @@ func NewCommandArg(text string) *CommandArg {
return &CommandArg{CommandValueAnyType, text, CommandRegexString, false} return &CommandArg{CommandValueAnyType, text, CommandRegexString, false}
} }
// SetValueType sets expected value type and switches built-in validation regexp.
func (c *CommandArg) SetValueType(t CommandValueType) *CommandArg { func (c *CommandArg) SetValueType(t CommandValueType) *CommandArg {
regex := CommandRegexString regex := CommandRegexString
switch t { switch t {
@@ -96,7 +97,7 @@ func NewCommand[T any](exec CommandExecutor[T], command string, args ...CommandA
} }
// NewPayload creates a new Command with the given executor, command payload string, and arguments. // NewPayload creates a new Command with the given executor, command payload string, and arguments.
// The command string can POTENTIALLY contain any symbols, but recommended to use only "_", "-", ".", a-Z, 0-9 // The command string can contain any symbols, but it is recommended to use only "_", "-", ".", a-z, A-Z, and 0-9.
func NewPayload[T any](exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] { func NewPayload[T any](exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] {
return &Command[T]{command, "", exec, args, make(extypes.Slice[Middleware[T]], 0), false} return &Command[T]{command, "", exec, args, make(extypes.Slice[Middleware[T]], 0), false}
} }
@@ -223,11 +224,6 @@ func (p *Plugin[T]) executeCmd(cmd string, ctx *MsgContext, dbContext *T) {
return return
} }
// Run plugin middlewares
if !p.executeMiddlewares(ctx, dbContext) {
return
}
// Run command-specific middlewares // Run command-specific middlewares
for _, m := range command.middlewares { for _, m := range command.middlewares {
if !m.Execute(ctx, dbContext) { if !m.Execute(ctx, dbContext) {
@@ -254,11 +250,6 @@ func (p *Plugin[T]) executePayload(payload string, ctx *MsgContext, dbContext *T
return return
} }
// Run plugin middlewares
if !p.executeMiddlewares(ctx, dbContext) {
return
}
// Run command-specific middlewares // Run command-specific middlewares
for _, m := range command.middlewares { for _, m := range command.middlewares {
if !m.Execute(ctx, dbContext) { if !m.Execute(ctx, dbContext) {

View File

@@ -98,12 +98,15 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
continue continue
} }
if !runner.onetime && runner.async && runner.timeout == 0 { if !runner.onetime && runner.async && runner.timeout == 0 {
bot.logger.Warnf("Background runner \"%s\" has no timeout — may cause tight loop\n", runner.name) bot.logger.Warnf("Background runner \"%s\" has no timeout — skipping\n", runner.name)
continue
} }
if runner.onetime && runner.async { if runner.onetime && runner.async {
// One-time async: fire and forget // One-time async: fire and forget
bot.runnerOnceWG.Add(1)
go func(r Runner[T]) { go func(r Runner[T]) {
defer bot.runnerOnceWG.Done()
err := r.fn(bot) err := r.fn(bot)
if err != nil { if err != nil {
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err) bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
@@ -122,7 +125,9 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
} }
} else if !runner.onetime && runner.async { } else if !runner.onetime && runner.async {
// Background loop: periodic execution with graceful shutdown // Background loop: periodic execution with graceful shutdown
bot.runnerBgWG.Add(1)
go func(r Runner[T]) { go func(r Runner[T]) {
defer bot.runnerBgWG.Done()
ticker := time.NewTicker(r.timeout) ticker := time.NewTicker(r.timeout)
defer ticker.Stop() defer ticker.Stop()
for { for {

View File

@@ -76,7 +76,11 @@ func (opts *APIOpts) SetLimiterDrop(b bool) *APIOpts {
return opts return opts
} }
// API is the main Telegram Bot API client. // API is the main Telegram Bot API client for JSON requests.
//
// Use API methods when sending JSON payloads (for example with file_id, URL, or other
// non-multipart fields). For multipart file uploads, use Uploader.
//
// It manages HTTP requests, rate limiting, retries, and connection pooling. // It manages HTTP requests, rate limiting, retries, and connection pooling.
type API struct { type API struct {
token string token string
@@ -102,7 +106,7 @@ func NewAPI(opts *APIOpts) *API {
} }
pool := newWorkerPool(16, 256) pool := newWorkerPool(16, 256)
pool.start(context.Background()) pool.start()
return &API{ return &API{
token: opts.token, token: opts.token,
@@ -118,12 +122,14 @@ func NewAPI(opts *APIOpts) *API {
// CloseApi shuts down the internal worker pool and closes the logger. // CloseApi shuts down the internal worker pool and closes the logger.
// Must be called to avoid resource leaks. // Must be called to avoid resource leaks.
// See https://core.telegram.org/bots/api
func (api *API) CloseApi() error { func (api *API) CloseApi() error {
api.pool.stop() api.pool.stop()
return api.logger.Close() return api.logger.Close()
} }
// GetLogger returns the internal logger for custom logging. // GetLogger returns the internal logger for custom logging.
// See https://core.telegram.org/bots/api
func (api *API) GetLogger() *slog.Logger { func (api *API) GetLogger() *slog.Logger {
return api.logger return api.logger
} }

View File

@@ -15,7 +15,7 @@ type SendPhotoP struct {
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"` HasSpoiler bool `json:"has_spoiler,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"` DisableNotifications bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"` AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"` MessageEffectID string `json:"message_effect_id,omitempty"`
@@ -107,7 +107,7 @@ type SendVideoP struct {
Duration int `json:"duration,omitempty"` Duration int `json:"duration,omitempty"`
Width int `json:"width,omitempty"` Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"` Height int `json:"height,omitempty"`
Cover int `json:"cover,omitempty"` Cover string `json:"cover,omitempty"`
StartTimestamp int `json:"start_timestamp,omitempty"` StartTimestamp int `json:"start_timestamp,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
@@ -276,7 +276,7 @@ type SendMediaGroupP struct {
// SendMediaGroup sends a group of photos, videos, documents or audios as an album. // SendMediaGroup sends a group of photos, videos, documents or audios as an album.
// See https://core.telegram.org/bots/api#sendmediagroup // See https://core.telegram.org/bots/api#sendmediagroup
func (api *API) SendMediaGroup(params SendMediaGroupP) (Message, error) { func (api *API) SendMediaGroup(params SendMediaGroupP) ([]Message, error) {
req := NewRequestWithChatID[Message]("sendMediaGroup", params, params.ChatID) req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }

View File

@@ -70,5 +70,5 @@ type PhotoSize struct {
FileUniqueID string `json:"file_unique_id"` FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
FileSize int `json:"file_size,omitempty"` FileSize int64 `json:"file_size,omitempty"`
} }

View File

@@ -154,7 +154,7 @@ func (api *API) RemoveMyProfilePhoto() (bool, error) {
// SetChatMenuButtonP holds parameters for the setChatMenuButton method. // SetChatMenuButtonP holds parameters for the setChatMenuButton method.
// See https://core.telegram.org/bots/api#setchatmenubutton // See https://core.telegram.org/bots/api#setchatmenubutton
type SetChatMenuButtonP struct { type SetChatMenuButtonP struct {
ChatID int `json:"chat_id"` ChatID int64 `json:"chat_id"`
MenuButton MenuButtonType `json:"menu_button"` MenuButton MenuButtonType `json:"menu_button"`
} }
@@ -169,7 +169,7 @@ func (api *API) SetChatMenuButton(params SetChatMenuButtonP) (bool, error) {
// GetChatMenuButtonP holds parameters for the getChatMenuButton method. // GetChatMenuButtonP holds parameters for the getChatMenuButton method.
// See https://core.telegram.org/bots/api#getchatmenubutton // See https://core.telegram.org/bots/api#getchatmenubutton
type GetChatMenuButtonP struct { type GetChatMenuButtonP struct {
ChatID int `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// GetChatMenuButton returns the current menu button for the given chat. // GetChatMenuButton returns the current menu button for the given chat.
@@ -217,8 +217,8 @@ func (api *API) GetAvailableGifts() (Gifts, error) {
// SendGiftP holds parameters for the sendGift method. // SendGiftP holds parameters for the sendGift method.
// See https://core.telegram.org/bots/api#sendgift // See https://core.telegram.org/bots/api#sendgift
type SendGiftP struct { type SendGiftP struct {
UserID int `json:"user_id,omitempty"` UserID int64 `json:"user_id,omitempty"`
ChatID int `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
GiftID string `json:"gift_id"` GiftID string `json:"gift_id"`
PayForUpgrade bool `json:"pay_for_upgrade"` PayForUpgrade bool `json:"pay_for_upgrade"`
Text string `json:"text"` Text string `json:"text"`
@@ -237,7 +237,7 @@ func (api *API) SendGift(params SendGiftP) (bool, error) {
// GiftPremiumSubscriptionP holds parameters for the giftPremiumSubscription method. // GiftPremiumSubscriptionP holds parameters for the giftPremiumSubscription method.
// See https://core.telegram.org/bots/api#giftpremiumsubscription // See https://core.telegram.org/bots/api#giftpremiumsubscription
type GiftPremiumSubscriptionP struct { type GiftPremiumSubscriptionP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
MonthCount int `json:"month_count"` MonthCount int `json:"month_count"`
StarCount int `json:"star_count"` StarCount int `json:"star_count"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`

View File

@@ -31,8 +31,8 @@ const (
// See https://core.telegram.org/bots/api#botcommandscope // See https://core.telegram.org/bots/api#botcommandscope
type BotCommandScope struct { type BotCommandScope struct {
Type BotCommandScopeType `json:"type"` Type BotCommandScopeType `json:"type"`
ChatID *int `json:"chat_id,omitempty"` ChatID *int64 `json:"chat_id,omitempty"`
UserID *int `json:"user_id,omitempty"` UserID *int64 `json:"user_id,omitempty"`
} }
// BotName represents the bot's name. // BotName represents the bot's name.

View File

@@ -3,7 +3,7 @@ package tgapi
// VerifyUserP holds parameters for the verifyUser method. // VerifyUserP holds parameters for the verifyUser method.
// See https://core.telegram.org/bots/api#verifyuser // See https://core.telegram.org/bots/api#verifyuser
type VerifyUserP struct { type VerifyUserP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
CustomDescription string `json:"custom_description,omitempty"` CustomDescription string `json:"custom_description,omitempty"`
} }
@@ -18,7 +18,7 @@ func (api *API) VerifyUser(params VerifyUserP) (bool, error) {
// VerifyChatP holds parameters for the verifyChat method. // VerifyChatP holds parameters for the verifyChat method.
// See https://core.telegram.org/bots/api#verifychat // See https://core.telegram.org/bots/api#verifychat
type VerifyChatP struct { type VerifyChatP struct {
ChatID int `json:"chat_id"` ChatID int64 `json:"chat_id"`
CustomDescription string `json:"custom_description,omitempty"` CustomDescription string `json:"custom_description,omitempty"`
} }
@@ -33,7 +33,7 @@ func (api *API) VerifyChat(params VerifyChatP) (bool, error) {
// RemoveUserVerificationP holds parameters for the removeUserVerification method. // RemoveUserVerificationP holds parameters for the removeUserVerification method.
// See https://core.telegram.org/bots/api#removeuserverification // See https://core.telegram.org/bots/api#removeuserverification
type RemoveUserVerificationP struct { type RemoveUserVerificationP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
} }
// RemoveUserVerification removes a user's verification. // RemoveUserVerification removes a user's verification.
@@ -47,7 +47,7 @@ func (api *API) RemoveUserVerification(params RemoveUserVerificationP) (bool, er
// RemoveChatVerificationP holds parameters for the removeChatVerification method. // RemoveChatVerificationP holds parameters for the removeChatVerification method.
// See https://core.telegram.org/bots/api#removechatverification // See https://core.telegram.org/bots/api#removechatverification
type RemoveChatVerificationP struct { type RemoveChatVerificationP struct {
ChatID int `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// RemoveChatVerification removes a chat's verification. // RemoveChatVerification removes a chat's verification.
@@ -62,7 +62,7 @@ func (api *API) RemoveChatVerification(params RemoveChatVerificationP) (bool, er
// See https://core.telegram.org/bots/api#readbusinessmessage // See https://core.telegram.org/bots/api#readbusinessmessage
type ReadBusinessMessageP struct { type ReadBusinessMessageP struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ChatID int `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
} }
@@ -74,18 +74,31 @@ func (api *API) ReadBusinessMessage(params ReadBusinessMessageP) (bool, error) {
return req.Do(api) return req.Do(api)
} }
// DeleteBusinessMessageP holds parameters for the deleteBusinessMessage method. // GetBusinessConnectionP holds parameters for the getBusinessConnection method.
// See https://core.telegram.org/bots/api#deletebusinessmessage // See https://core.telegram.org/bots/api#getbusinessconnection
type DeleteBusinessMessageP struct { type GetBusinessConnectionP struct {
BusinessConnectionID string `json:"business_connection_id"`
}
// GetBusinessConnection returns information about a business connection.
// See https://core.telegram.org/bots/api#getbusinessconnection
func (api *API) GetBusinessConnection(params GetBusinessConnectionP) (BusinessConnection, error) {
req := NewRequest[BusinessConnection]("getBusinessConnection", params)
return req.Do(api)
}
// DeleteBusinessMessagesP holds parameters for the deleteBusinessMessages method.
// See https://core.telegram.org/bots/api#deletebusinessmessages
type DeleteBusinessMessagesP struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
MessageIDs []int `json:"message_ids"` MessageIDs []int `json:"message_ids"`
} }
// DeleteBusinessMessage deletes business messages. // DeleteBusinessMessages deletes business messages.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#deletebusinessmessage // See https://core.telegram.org/bots/api#deletebusinessmessages
func (api *API) DeleteBusinessMessage(params DeleteBusinessMessageP) (bool, error) { func (api *API) DeleteBusinessMessages(params DeleteBusinessMessagesP) (bool, error) {
req := NewRequest[bool]("deleteBusinessMessage", params) req := NewRequest[bool]("deleteBusinessMessages", params)
return req.Do(api) return req.Do(api)
} }
@@ -191,22 +204,22 @@ type GetBusinessAccountStarBalanceP struct {
// GetBusinessAccountStarBalance returns the star balance of a business account. // GetBusinessAccountStarBalance returns the star balance of a business account.
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance // See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalanceP) (StarAmount, error) { func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalanceP) (StarAmount, error) {
req := NewRequest[StarAmount]("getBusinessAccountGiftSettings", params) // Note: method name in call is incorrect, should be "getBusinessAccountStarBalance". We'll keep as is, but comment refers to correct. req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params)
return req.Do(api) return req.Do(api)
} }
// TransferBusinessAccountStartP holds parameters for the transferBusinessAccountStart method. // TransferBusinessAccountStarsP holds parameters for the transferBusinessAccountStars method.
// See https://core.telegram.org/bots/api#transferbusinessaccountstart // See https://core.telegram.org/bots/api#transferbusinessaccountstars
type TransferBusinessAccountStartP struct { type TransferBusinessAccountStarsP struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
StarCount int `json:"star_count"` StarCount int `json:"star_count"`
} }
// TransferBusinessAccountStart transfers stars from a business account. // TransferBusinessAccountStars transfers stars from a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#transferbusinessaccountstart // See https://core.telegram.org/bots/api#transferbusinessaccountstars
func (api *API) TransferBusinessAccountStart(params TransferBusinessAccountStartP) (bool, error) { func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStarsP) (bool, error) {
req := NewRequest[bool]("transferBusinessAccountStart", params) req := NewRequest[bool]("transferBusinessAccountStars", params)
return req.Do(api) return req.Do(api)
} }
@@ -270,7 +283,7 @@ func (api *API) UpgradeGift(params UpgradeGiftP) (bool, error) {
type TransferGiftP struct { type TransferGiftP struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
OwnedGiftID string `json:"owned_gift_id"` OwnedGiftID string `json:"owned_gift_id"`
NewOwnerChatID int `json:"new_owner_chat_id"` NewOwnerChatID int64 `json:"new_owner_chat_id"`
StarCount int `json:"star_count,omitempty"` StarCount int `json:"star_count,omitempty"`
} }
@@ -316,7 +329,7 @@ func (api *API) PostStoryVideo(params PostStoryP) (Story, error) {
// See https://core.telegram.org/bots/api#repoststory // See https://core.telegram.org/bots/api#repoststory
type RepostStoryP struct { type RepostStoryP struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
FromChatID int `json:"from_chat_id"` FromChatID int64 `json:"from_chat_id"`
FromStoryID int `json:"from_story_id"` FromStoryID int `json:"from_story_id"`
ActivePeriod int `json:"active_period"` ActivePeriod int `json:"active_period"`
PostToChatPage bool `json:"post_to_chat_page,omitempty"` PostToChatPage bool `json:"post_to_chat_page,omitempty"`

View File

@@ -54,7 +54,7 @@ type BusinessBotRights struct {
type BusinessConnection struct { type BusinessConnection struct {
ID string `json:"id"` ID string `json:"id"`
User User `json:"user"` User User `json:"user"`
UserChatID int `json:"user_chat_id"` UserChatID int64 `json:"user_chat_id"`
Date int `json:"date"` Date int `json:"date"`
Rights *BusinessBotRights `json:"rights,omitempty"` Rights *BusinessBotRights `json:"rights,omitempty"`
IsEnabled bool `json:"is_enabled"` IsEnabled bool `json:"is_enabled"`

View File

@@ -4,7 +4,7 @@ package tgapi
// See https://core.telegram.org/bots/api#banchatmember // See https://core.telegram.org/bots/api#banchatmember
type BanChatMemberP struct { type BanChatMemberP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
UntilDate int `json:"until_date,omitempty"` UntilDate int `json:"until_date,omitempty"`
RevokeMessages bool `json:"revoke_messages,omitempty"` RevokeMessages bool `json:"revoke_messages,omitempty"`
} }
@@ -21,7 +21,7 @@ func (api *API) BanChatMember(params BanChatMemberP) (bool, error) {
// See https://core.telegram.org/bots/api#unbanchatmember // See https://core.telegram.org/bots/api#unbanchatmember
type UnbanChatMemberP struct { type UnbanChatMemberP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
OnlyIfBanned bool `json:"only_if_banned"` OnlyIfBanned bool `json:"only_if_banned"`
} }
@@ -37,7 +37,7 @@ func (api *API) UnbanChatMember(params UnbanChatMemberP) (bool, error) {
// See https://core.telegram.org/bots/api#restrictchatmember // See https://core.telegram.org/bots/api#restrictchatmember
type RestrictChatMemberP struct { type RestrictChatMemberP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Permissions ChatPermissions `json:"permissions"` Permissions ChatPermissions `json:"permissions"`
UseIndependentChatPermissions bool `json:"use_independent_chat_permissions,omitempty"` UseIndependentChatPermissions bool `json:"use_independent_chat_permissions,omitempty"`
UntilDate int `json:"until_date,omitempty"` UntilDate int `json:"until_date,omitempty"`
@@ -55,7 +55,7 @@ func (api *API) RestrictChatMember(params RestrictChatMemberP) (bool, error) {
// See https://core.telegram.org/bots/api#promotechatmember // See https://core.telegram.org/bots/api#promotechatmember
type PromoteChatMember struct { type PromoteChatMember struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
IsAnonymous bool `json:"is_anonymous,omitempty"` IsAnonymous bool `json:"is_anonymous,omitempty"`
CanManageChat bool `json:"can_manage_chat,omitempty"` CanManageChat bool `json:"can_manage_chat,omitempty"`
@@ -88,7 +88,7 @@ func (api *API) PromoteChatMember(params PromoteChatMember) (bool, error) {
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle // See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
type SetChatAdministratorCustomTitleP struct { type SetChatAdministratorCustomTitleP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
CustomTitle string `json:"custom_title"` CustomTitle string `json:"custom_title"`
} }
@@ -104,7 +104,7 @@ func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCusto
// See https://core.telegram.org/bots/api#setchatmembertag // See https://core.telegram.org/bots/api#setchatmembertag
type SetChatMemberTagP struct { type SetChatMemberTagP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`
} }
@@ -183,7 +183,7 @@ type CreateChatInviteLinkP struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
ExpireDate int `json:"expire_date,omitempty"` ExpireDate int `json:"expire_date,omitempty"`
MemberLimit int `json:"member_limit,omitempty"` MemberLimit int `json:"member_limit,omitempty"`
CreatesJoinRequest int `json:"creates_join_request,omitempty"` CreatesJoinRequest bool `json:"creates_join_request,omitempty"`
} }
// CreateChatInviteLink creates an additional invite link for a chat. // CreateChatInviteLink creates an additional invite link for a chat.
@@ -203,7 +203,7 @@ type EditChatInviteLinkP struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
ExpireDate int `json:"expire_date,omitempty"` ExpireDate int `json:"expire_date,omitempty"`
MemberLimit int `json:"member_limit,omitempty"` MemberLimit int `json:"member_limit,omitempty"`
CreatesJoinRequest int `json:"creates_join_request,omitempty"` CreatesJoinRequest bool `json:"creates_join_request,omitempty"`
} }
// EditChatInviteLink edits a nonprimary invite link. // EditChatInviteLink edits a nonprimary invite link.
@@ -266,7 +266,7 @@ func (api *API) RevokeChatInviteLink(params RevokeChatInviteLinkP) (ChatInviteLi
// See https://core.telegram.org/bots/api#approvechatjoinrequest // See https://core.telegram.org/bots/api#approvechatjoinrequest
type ApproveChatJoinRequestP struct { type ApproveChatJoinRequestP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
} }
// ApproveChatJoinRequest approves a chat join request. // ApproveChatJoinRequest approves a chat join request.
@@ -281,7 +281,7 @@ func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequestP) (bool, er
// See https://core.telegram.org/bots/api#declinechatjoinrequest // See https://core.telegram.org/bots/api#declinechatjoinrequest
type DeclineChatJoinRequestP struct { type DeclineChatJoinRequestP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
} }
// DeclineChatJoinRequest declines a chat join request. // DeclineChatJoinRequest declines a chat join request.
@@ -292,13 +292,23 @@ func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequestP) (bool, er
return req.Do(api) return req.Do(api)
} }
// SetChatPhoto is a stub method (needs implementation). // SetChatPhotoP holds parameters for the setChatPhoto method.
// Currently incomplete. // See https://core.telegram.org/bots/api#setchatphoto
func (api *API) SetChatPhoto() { type SetChatPhotoP struct {
ChatID int64 `json:"chat_id"`
}
// SetChatPhoto changes the chat photo.
// photo is the file to upload as the new photo.
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatphoto
func (api *API) SetChatPhoto(params SetChatPhotoP, photo UploaderFile) (bool, error) {
uploader := NewUploader(api) uploader := NewUploader(api)
defer func() { defer func() {
_ = uploader.Close() _ = uploader.Close()
}() }()
req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo.SetType(UploaderPhotoType))
return req.Do(uploader)
} }
// DeleteChatPhotoP holds parameters for the deleteChatPhoto method. // DeleteChatPhotoP holds parameters for the deleteChatPhoto method.
@@ -449,7 +459,7 @@ func (api *API) GetChatMemberCount(params GetChatMembersCountP) (int, error) {
// See https://core.telegram.org/bots/api#getchatmember // See https://core.telegram.org/bots/api#getchatmember
type GetChatMemberP struct { type GetChatMemberP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
} }
// GetChatMember returns information about a member of a chat. // GetChatMember returns information about a member of a chat.
@@ -492,7 +502,7 @@ func (api *API) DeleteChatStickerSet(params DeleteChatStickerSetP) (bool, error)
// See https://core.telegram.org/bots/api#getuserchatboosts // See https://core.telegram.org/bots/api#getuserchatboosts
type GetUserChatBoostsP struct { type GetUserChatBoostsP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
} }
// GetUserChatBoosts returns the list of boosts a user has given to a chat. // GetUserChatBoosts returns the list of boosts a user has given to a chat.

View File

@@ -26,7 +26,7 @@ const (
// ChatFullInfo contains full information about a chat. // ChatFullInfo contains full information about a chat.
// See https://core.telegram.org/bots/api#chatfullinfo // See https://core.telegram.org/bots/api#chatfullinfo
type ChatFullInfo struct { type ChatFullInfo struct {
ID int `json:"id"` ID int64 `json:"id"`
Type ChatType `json:"type"` Type ChatType `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Username string `json:"username"` Username string `json:"username"`
@@ -78,7 +78,7 @@ type ChatFullInfo struct {
StickerSetName *string `json:"sticker_set_name,omitempty"` StickerSetName *string `json:"sticker_set_name,omitempty"`
CanSetStickerSet *bool `json:"can_set_sticker_set,omitempty"` CanSetStickerSet *bool `json:"can_set_sticker_set,omitempty"`
CustomEmojiStickerSetName *string `json:"custom_emoji_sticker_set_name,omitempty"` CustomEmojiStickerSetName *string `json:"custom_emoji_sticker_set_name,omitempty"`
LinkedChatID *int `json:"linked_chat_id,omitempty"` LinkedChatID *int64 `json:"linked_chat_id,omitempty"`
Location *ChatLocation `json:"location,omitempty"` Location *ChatLocation `json:"location,omitempty"`
Rating *UserRating `json:"rating,omitempty"` Rating *UserRating `json:"rating,omitempty"`
@@ -108,7 +108,7 @@ type ChatPermissions struct {
CanSendPolls bool `json:"can_send_polls"` CanSendPolls bool `json:"can_send_polls"`
CanSendOtherMessages bool `json:"can_send_other_messages"` CanSendOtherMessages bool `json:"can_send_other_messages"`
CanAddWebPagePreview bool `json:"can_add_web_page_preview"` CanAddWebPagePreview bool `json:"can_add_web_page_preview"`
CatEditTag bool `json:"cat_edit_tag"` // Note: field name likely a typo, should be "can_edit_tag" CanEditTag bool `json:"can_edit_tag"`
CanChangeInfo bool `json:"can_change_info"` CanChangeInfo bool `json:"can_change_info"`
CanInviteUsers bool `json:"can_invite_users"` CanInviteUsers bool `json:"can_invite_users"`
CanPinMessages bool `json:"can_pin_messages"` CanPinMessages bool `json:"can_pin_messages"`
@@ -127,7 +127,7 @@ type ChatLocation struct {
type ChatInviteLink struct { type ChatInviteLink struct {
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
Creator User `json:"creator"` Creator User `json:"creator"`
CreateJoinRequest bool `json:"create_join_request"` CreateJoinRequest bool `json:"creates_join_request"`
IsPrimary bool `json:"is_primary"` IsPrimary bool `json:"is_primary"`
IsRevoked bool `json:"is_revoked"` IsRevoked bool `json:"is_revoked"`

View File

@@ -5,3 +5,4 @@ import "errors"
var ErrRateLimit = errors.New("rate limit exceeded") var ErrRateLimit = errors.New("rate limit exceeded")
var ErrPoolUnexpected = errors.New("unexpected response from pool") var ErrPoolUnexpected = errors.New("unexpected response from pool")
var ErrPoolQueueFull = errors.New("worker pool queue full") var ErrPoolQueueFull = errors.New("worker pool queue full")
var ErrPoolStopped = errors.New("worker pool stopped")

69
tgapi/games_methods.go Normal file
View File

@@ -0,0 +1,69 @@
package tgapi
// SendGameP holds parameters for the sendGame method.
// See https://core.telegram.org/bots/api#sendgame
type SendGameP struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
GameShortName string `json:"game_short_name"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// SendGame sends a game message.
// See https://core.telegram.org/bots/api#sendgame
func (api *API) SendGame(params SendGameP) (Message, error) {
req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID)
return req.Do(api)
}
// SetGameScoreP holds parameters for the setGameScore method.
// See https://core.telegram.org/bots/api#setgamescore
type SetGameScoreP struct {
UserID int64 `json:"user_id"`
Score int `json:"score"`
Force bool `json:"force,omitempty"`
DisableEditMessage bool `json:"disable_edit_message,omitempty"`
ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"`
InlineMessageID string `json:"inline_message_id,omitempty"`
}
// SetGameScore sets a user's score in a game message.
// If inline_message_id is provided, returns a boolean success flag.
// Otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#setgamescore
func (api *API) SetGameScore(params SetGameScoreP) (Message, bool, error) {
var zero Message
if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("setGameScore", params, params.ChatID)
res, err := req.Do(api)
return zero, res, err
}
req := NewRequestWithChatID[Message]("setGameScore", params, params.ChatID)
res, err := req.Do(api)
return res, false, err
}
// GetGameHighScoresP holds parameters for the getGameHighScores method.
// See https://core.telegram.org/bots/api#getgamehighscores
type GetGameHighScoresP struct {
UserID int64 `json:"user_id"`
ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"`
InlineMessageID string `json:"inline_message_id,omitempty"`
}
// GetGameHighScores returns game high score data for a user.
// See https://core.telegram.org/bots/api#getgamehighscores
func (api *API) GetGameHighScores(params GetGameHighScoresP) ([]GameHighScore, error) {
req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID)
return req.Do(api)
}

9
tgapi/games_types.go Normal file
View File

@@ -0,0 +1,9 @@
package tgapi
// GameHighScore represents one row in a game high score table.
// See https://core.telegram.org/bots/api#gamehighscore
type GameHighScore struct {
Position int `json:"position"`
User User `json:"user"`
Score int `json:"score"`
}

52
tgapi/inline_methods.go Normal file
View File

@@ -0,0 +1,52 @@
package tgapi
// AnswerInlineQueryP holds parameters for the answerInlineQuery method.
// See https://core.telegram.org/bots/api#answerinlinequery
type AnswerInlineQueryP struct {
InlineQueryID string `json:"inline_query_id"`
Results []InlineQueryResult `json:"results"`
CacheTime int `json:"cache_time,omitempty"`
IsPersonal bool `json:"is_personal,omitempty"`
NextOffset string `json:"next_offset,omitempty"`
Button *InlineQueryResultsButton `json:"button,omitempty"`
}
// AnswerInlineQuery sends answers to an inline query.
// Returns true on success.
// See https://core.telegram.org/bots/api#answerinlinequery
func (api *API) AnswerInlineQuery(params AnswerInlineQueryP) (bool, error) {
req := NewRequest[bool]("answerInlineQuery", params)
return req.Do(api)
}
// AnswerWebAppQueryP holds parameters for the answerWebAppQuery method.
// See https://core.telegram.org/bots/api#answerwebappquery
type AnswerWebAppQueryP struct {
WebAppQueryID string `json:"web_app_query_id"`
Result InlineQueryResult `json:"result"`
}
// AnswerWebAppQuery sets the result of a Web App interaction.
// See https://core.telegram.org/bots/api#answerwebappquery
func (api *API) AnswerWebAppQuery(params AnswerWebAppQueryP) (SentWebAppMessage, error) {
req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params)
return req.Do(api)
}
// SavePreparedInlineMessageP holds parameters for the savePreparedInlineMessage method.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage
type SavePreparedInlineMessageP struct {
UserID int64 `json:"user_id"`
Result InlineQueryResult `json:"result"`
AllowUserChats bool `json:"allow_user_chats,omitempty"`
AllowBotChats bool `json:"allow_bot_chats,omitempty"`
AllowGroupChats bool `json:"allow_group_chats,omitempty"`
AllowChannelChats bool `json:"allow_channel_chats,omitempty"`
}
// SavePreparedInlineMessage stores a prepared message for Mini App users.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage
func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessageP) (PreparedInlineMessage, error) {
req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params)
return req.Do(api)
}

26
tgapi/inline_types.go Normal file
View File

@@ -0,0 +1,26 @@
package tgapi
// InlineQueryResult is a JSON-serializable inline query result object.
// See https://core.telegram.org/bots/api#inlinequeryresult
type InlineQueryResult map[string]any
// InlineQueryResultsButton represents a button shown above inline query results.
// See https://core.telegram.org/bots/api#inlinequeryresultsbutton
type InlineQueryResultsButton struct {
Text string `json:"text"`
WebApp *WebAppInfo `json:"web_app,omitempty"`
StartParameter string `json:"start_parameter,omitempty"`
}
// SentWebAppMessage describes an inline message sent by a Web App on behalf of a user.
// See https://core.telegram.org/bots/api#sentwebappmessage
type SentWebAppMessage struct {
InlineMessageID string `json:"inline_message_id,omitempty"`
}
// PreparedInlineMessage describes a prepared inline message.
// See https://core.telegram.org/bots/api#preparedinlinemessage
type PreparedInlineMessage struct {
ID string `json:"id"`
ExpirationDate int `json:"expiration_date"`
}

View File

@@ -12,7 +12,7 @@ type SendMessageP struct {
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
Entities []MessageEntity `json:"entities,omitempty"` Entities []MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"` DisableNotifications bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"` AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"` MessageEffectID string `json:"message_effect_id,omitempty"`
@@ -69,8 +69,8 @@ type ForwardMessagesP struct {
// ForwardMessages forwards multiple messages. // ForwardMessages forwards multiple messages.
// Returns an array of message IDs of the sent messages. // Returns an array of message IDs of the sent messages.
// See https://core.telegram.org/bots/api#forwardmessages // See https://core.telegram.org/bots/api#forwardmessages
func (api *API) ForwardMessages(params ForwardMessagesP) ([]int, error) { func (api *API) ForwardMessages(params ForwardMessagesP) ([]MessageID, error) {
req := NewRequestWithChatID[[]int]("forwardMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("forwardMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -103,8 +103,11 @@ type CopyMessageP struct {
// Returns the MessageID of the sent copy. // Returns the MessageID of the sent copy.
// See https://core.telegram.org/bots/api#copymessage // See https://core.telegram.org/bots/api#copymessage
func (api *API) CopyMessage(params CopyMessageP) (int, error) { func (api *API) CopyMessage(params CopyMessageP) (int, error) {
req := NewRequestWithChatID[int]("copyMessage", params, params.ChatID) msgID, err := NewRequestWithChatID[MessageID]("copyMessage", params, params.ChatID).Do(api)
return req.Do(api) if err != nil {
return 0, err
}
return msgID.MessageID, nil
} }
// CopyMessagesP holds parameters for the copyMessages method. // CopyMessagesP holds parameters for the copyMessages method.
@@ -124,15 +127,15 @@ type CopyMessagesP struct {
// CopyMessages copies multiple messages. // CopyMessages copies multiple messages.
// Returns an array of message IDs of the sent copies. // Returns an array of message IDs of the sent copies.
// See https://core.telegram.org/bots/api#copymessages // See https://core.telegram.org/bots/api#copymessages
func (api *API) CopyMessages(params CopyMessagesP) ([]int, error) { func (api *API) CopyMessages(params CopyMessagesP) ([]MessageID, error) {
req := NewRequestWithChatID[[]int]("copyMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("copyMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
// SendLocationP holds parameters for the sendLocation method. // SendLocationP holds parameters for the sendLocation method.
// See https://core.telegram.org/bots/api#sendlocation // See https://core.telegram.org/bots/api#sendlocation
type SendLocationP struct { type SendLocationP struct {
BusinessConnectionID int `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -164,7 +167,7 @@ func (api *API) SendLocation(params SendLocationP) (Message, error) {
// SendVenueP holds parameters for the sendVenue method. // SendVenueP holds parameters for the sendVenue method.
// See https://core.telegram.org/bots/api#sendvenue // See https://core.telegram.org/bots/api#sendvenue
type SendVenueP struct { type SendVenueP struct {
BusinessConnectionID int `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -198,7 +201,7 @@ func (api *API) SendVenue(params SendVenueP) (Message, error) {
// SendContactP holds parameters for the sendContact method. // SendContactP holds parameters for the sendContact method.
// See https://core.telegram.org/bots/api#sendcontact // See https://core.telegram.org/bots/api#sendcontact
type SendContactP struct { type SendContactP struct {
BusinessConnectionID int `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -228,12 +231,12 @@ func (api *API) SendContact(params SendContactP) (Message, error) {
// SendPollP holds parameters for the sendPoll method. // SendPollP holds parameters for the sendPoll method.
// See https://core.telegram.org/bots/api#sendpoll // See https://core.telegram.org/bots/api#sendpoll
type SendPollP struct { type SendPollP struct {
BusinessConnectionID int `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
Question string `json:"question"` Question string `json:"question"`
QuestionParseMode ParseMode `json:"question_mode,omitempty"` QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"`
QuestionEntities []MessageEntity `json:"question_entities,omitempty"` QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
Options []InputPollOption `json:"options"` Options []InputPollOption `json:"options"`
IsAnonymous bool `json:"is_anonymous,omitempty"` IsAnonymous bool `json:"is_anonymous,omitempty"`
@@ -266,7 +269,7 @@ func (api *API) SendPoll(params SendPollP) (Message, error) {
// SendChecklistP holds parameters for the sendChecklist method. // SendChecklistP holds parameters for the sendChecklist method.
// See https://core.telegram.org/bots/api#sendchecklist // See https://core.telegram.org/bots/api#sendchecklist
type SendChecklistP struct { type SendChecklistP struct {
BusinessConnectionID int `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Checklist InputChecklist `json:"checklist"` Checklist InputChecklist `json:"checklist"`
@@ -288,7 +291,7 @@ func (api *API) SendChecklist(params SendChecklistP) (Message, error) {
// SendDiceP holds parameters for the sendDice method. // SendDiceP holds parameters for the sendDice method.
// See https://core.telegram.org/bots/api#senddice // See https://core.telegram.org/bots/api#senddice
type SendDiceP struct { type SendDiceP struct {
BusinessConnectionID int `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -313,6 +316,7 @@ func (api *API) SendDice(params SendDiceP) (Message, error) {
} }
// SendMessageDraftP holds parameters for the sendMessageDraft method. // SendMessageDraftP holds parameters for the sendMessageDraft method.
// See https://core.telegram.org/bots/api#sendmessagedraft
type SendMessageDraftP struct { type SendMessageDraftP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -322,7 +326,9 @@ type SendMessageDraftP struct {
Entities []MessageEntity `json:"entities,omitempty"` Entities []MessageEntity `json:"entities,omitempty"`
} }
// SendMessageDraft sends a previously saved draft message. // SendMessageDraft sends or updates a draft message in the target chat.
// Returns True on success.
// See https://core.telegram.org/bots/api#sendmessagedraft
func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) { func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) {
req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID) req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID)
return req.Do(api) return req.Do(api)
@@ -425,7 +431,7 @@ type EditMessageMediaP struct {
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
InlineMessageID string `json:"inline_message_id,omitempty"` InlineMessageID string `json:"inline_message_id,omitempty"`
Message InputMedia `json:"message"` Media InputMedia `json:"media"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }

View File

@@ -2,6 +2,11 @@ package tgapi
import "git.nix13.pw/scuroneko/extypes" import "git.nix13.pw/scuroneko/extypes"
// MessageID represents a message identifier wrapper returned by some API methods.
type MessageID struct {
MessageID int `json:"message_id"`
}
// MessageReplyMarkup represents an inline keyboard markup for a message. // MessageReplyMarkup represents an inline keyboard markup for a message.
// It is used in the Message type. // It is used in the Message type.
type MessageReplyMarkup struct { type MessageReplyMarkup struct {
@@ -114,7 +119,7 @@ type MessageEntity struct {
// See https://core.telegram.org/bots/api#replyparameters // See https://core.telegram.org/bots/api#replyparameters
type ReplyParameters struct { type ReplyParameters struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
ChatID int `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
Quote string `json:"quote,omitempty"` Quote string `json:"quote,omitempty"`
@@ -181,9 +186,11 @@ type ReplyKeyboardMarkup struct {
type CallbackQuery struct { type CallbackQuery struct {
ID string `json:"id"` ID string `json:"id"`
From User `json:"from"` From User `json:"from"`
Message Message `json:"message"` Message *Message `json:"message,omitempty"`
InlineMessageID *string `json:"inline_message_id,omitempty"`
Data string `json:"data"` ChatInstance string `json:"chat_instance,omitempty"`
Data string `json:"data,omitempty"`
GameShortName string `json:"game_short_name,omitempty"`
} }
// InputPollOption contains information about one answer option in a poll to be sent. // InputPollOption contains information about one answer option in a poll to be sent.

View File

@@ -6,26 +6,6 @@ import (
"net/http" "net/http"
) )
// ParseMode represents the text formatting mode for message parsing.
type ParseMode string
const (
// ParseMDV2 enables MarkdownV2 style parsing.
ParseMDV2 ParseMode = "MarkdownV2"
// ParseHTML enables HTML style parsing.
ParseHTML ParseMode = "HTML"
// ParseMD enables legacy Markdown style parsing.
ParseMD ParseMode = "Markdown"
// ParseNone disables any parsing.
ParseNone ParseMode = "None"
)
// EmptyParams is a placeholder for methods that take no parameters.
type EmptyParams struct{}
// NoParams is a convenient instance of EmptyParams.
var NoParams = EmptyParams{}
// UpdateParams holds parameters for the getUpdates method. // UpdateParams holds parameters for the getUpdates method.
// See https://core.telegram.org/bots/api#getupdates // See https://core.telegram.org/bots/api#getupdates
type UpdateParams struct { type UpdateParams struct {
@@ -65,6 +45,47 @@ func (api *API) GetUpdates(params UpdateParams) ([]Update, error) {
return req.Do(api) return req.Do(api)
} }
// SetWebhookP holds parameters for the setWebhook method.
// See https://core.telegram.org/bots/api#setwebhook
type SetWebhookP struct {
URL string `json:"url"`
Certificate string `json:"certificate,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
SecretToken string `json:"secret_token,omitempty"`
}
// SetWebhook sets a webhook URL for incoming updates.
// Returns true on success.
// See https://core.telegram.org/bots/api#setwebhook
func (api *API) SetWebhook(params SetWebhookP) (bool, error) {
req := NewRequest[bool]("setWebhook", params)
return req.Do(api)
}
// DeleteWebhookP holds parameters for the deleteWebhook method.
// See https://core.telegram.org/bots/api#deletewebhook
type DeleteWebhookP struct {
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
}
// DeleteWebhook removes the current webhook integration.
// Returns true on success.
// See https://core.telegram.org/bots/api#deletewebhook
func (api *API) DeleteWebhook(params DeleteWebhookP) (bool, error) {
req := NewRequest[bool]("deleteWebhook", params)
return req.Do(api)
}
// GetWebhookInfo returns the current webhook status.
// See https://core.telegram.org/bots/api#getwebhookinfo
func (api *API) GetWebhookInfo() (WebhookInfo, error) {
req := NewRequest[WebhookInfo]("getWebhookInfo", NoParams)
return req.Do(api)
}
// GetFileP holds parameters for the getFile method. // GetFileP holds parameters for the getFile method.
// See https://core.telegram.org/bots/api#getfile // See https://core.telegram.org/bots/api#getfile
type GetFileP struct { type GetFileP struct {

35
tgapi/methods_types.go Normal file
View File

@@ -0,0 +1,35 @@
package tgapi
// ParseMode represents the text formatting mode for message parsing.
type ParseMode string
const (
// ParseMDV2 enables MarkdownV2 style parsing.
ParseMDV2 ParseMode = "MarkdownV2"
// ParseHTML enables HTML style parsing.
ParseHTML ParseMode = "HTML"
// ParseMD enables legacy Markdown style parsing.
ParseMD ParseMode = "Markdown"
// ParseNone disables any parsing.
ParseNone ParseMode = "None"
)
// EmptyParams is a placeholder for methods that take no parameters.
type EmptyParams struct{}
// NoParams is a convenient instance of EmptyParams.
var NoParams = EmptyParams{}
// WebhookInfo describes the current webhook status.
// See https://core.telegram.org/bots/api#webhookinfo
type WebhookInfo struct {
URL string `json:"url"`
HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
IPAddress string `json:"ip_address,omitempty"`
LastErrorDate int `json:"last_error_date,omitempty"`
LastErrorMessage string `json:"last_error_message,omitempty"`
LastSynchronizationErrorDate int `json:"last_synchronization_error_date,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
AllowedUpdates []string `json:"allowed_updates,omitempty"`
}

16
tgapi/passport_methods.go Normal file
View File

@@ -0,0 +1,16 @@
package tgapi
// SetPassportDataErrorsP holds parameters for the setPassportDataErrors method.
// See https://core.telegram.org/bots/api#setpassportdataerrors
type SetPassportDataErrorsP struct {
UserID int64 `json:"user_id"`
Errors []PassportElementError `json:"errors"`
}
// SetPassportDataErrors informs a user about Telegram Passport data errors.
// Returns true on success.
// See https://core.telegram.org/bots/api#setpassportdataerrors
func (api *API) SetPassportDataErrors(params SetPassportDataErrorsP) (bool, error) {
req := NewRequest[bool]("setPassportDataErrors", params)
return req.Do(api)
}

5
tgapi/passport_types.go Normal file
View File

@@ -0,0 +1,5 @@
package tgapi
// PassportElementError is a JSON-serializable passport element error object.
// See https://core.telegram.org/bots/api#passportelementerror
type PassportElementError map[string]any

117
tgapi/payments_methods.go Normal file
View File

@@ -0,0 +1,117 @@
package tgapi
// SendInvoiceP holds parameters for the sendInvoice method.
// See https://core.telegram.org/bots/api#sendinvoice
type SendInvoiceP struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
Payload string `json:"payload"`
ProviderToken string `json:"provider_token,omitempty"`
Currency string `json:"currency"`
Prices []LabeledPrice `json:"prices"`
MaxTipAmount int `json:"max_tip_amount,omitempty"`
SuggestedTipAmounts []int `json:"suggested_tip_amounts,omitempty"`
StartParameter string `json:"start_parameter,omitempty"`
ProviderData string `json:"provider_data,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
PhotoSize int `json:"photo_size,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"`
NeedName bool `json:"need_name,omitempty"`
NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
NeedEmail bool `json:"need_email,omitempty"`
NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
SendPhoneToProvider bool `json:"send_phone_number_to_provider,omitempty"`
SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
IsFlexible bool `json:"is_flexible,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// SendInvoice sends an invoice.
// See https://core.telegram.org/bots/api#sendinvoice
func (api *API) SendInvoice(params SendInvoiceP) (Message, error) {
req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID)
return req.Do(api)
}
// CreateInvoiceLinkP holds parameters for the createInvoiceLink method.
// See https://core.telegram.org/bots/api#createinvoicelink
type CreateInvoiceLinkP struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
Payload string `json:"payload"`
ProviderToken string `json:"provider_token,omitempty"`
Currency string `json:"currency"`
Prices []LabeledPrice `json:"prices"`
SubscriptionPeriod int `json:"subscription_period,omitempty"`
MaxTipAmount int `json:"max_tip_amount,omitempty"`
SuggestedTipAmounts []int `json:"suggested_tip_amounts,omitempty"`
ProviderData string `json:"provider_data,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
PhotoSize int `json:"photo_size,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"`
NeedName bool `json:"need_name,omitempty"`
NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
NeedEmail bool `json:"need_email,omitempty"`
NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
SendPhoneToProvider bool `json:"send_phone_number_to_provider,omitempty"`
SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
IsFlexible bool `json:"is_flexible,omitempty"`
}
// CreateInvoiceLink creates an invoice link.
// See https://core.telegram.org/bots/api#createinvoicelink
func (api *API) CreateInvoiceLink(params CreateInvoiceLinkP) (string, error) {
req := NewRequest[string]("createInvoiceLink", params)
return req.Do(api)
}
// AnswerShippingQueryP holds parameters for the answerShippingQuery method.
// See https://core.telegram.org/bots/api#answershippingquery
type AnswerShippingQueryP struct {
ShippingQueryID string `json:"shipping_query_id"`
OK bool `json:"ok"`
ShippingOptions []ShippingOption `json:"shipping_options,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// AnswerShippingQuery answers a shipping query.
// Returns true on success.
// See https://core.telegram.org/bots/api#answershippingquery
func (api *API) AnswerShippingQuery(params AnswerShippingQueryP) (bool, error) {
req := NewRequest[bool]("answerShippingQuery", params)
return req.Do(api)
}
// AnswerPreCheckoutQueryP holds parameters for the answerPreCheckoutQuery method.
// See https://core.telegram.org/bots/api#answerprecheckoutquery
type AnswerPreCheckoutQueryP struct {
PreCheckoutQueryID string `json:"pre_checkout_query_id"`
OK bool `json:"ok"`
ErrorMessage string `json:"error_message,omitempty"`
}
// AnswerPreCheckoutQuery answers a pre-checkout query.
// Returns true on success.
// See https://core.telegram.org/bots/api#answerprecheckoutquery
func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQueryP) (bool, error) {
req := NewRequest[bool]("answerPreCheckoutQuery", params)
return req.Do(api)
}

16
tgapi/payments_types.go Normal file
View File

@@ -0,0 +1,16 @@
package tgapi
// LabeledPrice represents a price portion.
// See https://core.telegram.org/bots/api#labeledprice
type LabeledPrice struct {
Label string `json:"label"`
Amount int `json:"amount"`
}
// ShippingOption represents one shipping option.
// See https://core.telegram.org/bots/api#shippingoption
type ShippingOption struct {
ID string `json:"id"`
Title string `json:"title"`
Prices []LabeledPrice `json:"prices"`
}

View File

@@ -14,13 +14,16 @@ type workerPool struct {
workers int // количество воркеров (горутин) workers int // количество воркеров (горутин)
wg sync.WaitGroup // синхронизирует завершение всех воркеров при остановке wg sync.WaitGroup // синхронизирует завершение всех воркеров при остановке
quit chan struct{} // канал для сигнала остановки quit chan struct{} // канал для сигнала остановки
stopOnce sync.Once // гарантирует идемпотентную остановку пула
started bool // флаг, указывающий, запущен ли пул started bool // флаг, указывающий, запущен ли пул
stopped bool // флаг, указывающий, что пул остановлен
startedMu sync.Mutex // мьютекс для безопасного доступа к started startedMu sync.Mutex // мьютекс для безопасного доступа к started
} }
// requestEnvelope — приватная структура, инкапсулирующая задачу и канал для результата. // requestEnvelope — приватная структура, инкапсулирующая задачу и канал для результата.
// Используется только внутри пакета для передачи задач воркерам. // Используется только внутри пакета для передачи задач воркерам.
type requestEnvelope struct { type requestEnvelope struct {
ctx context.Context // контекст конкретной задачи
doFunc func(context.Context) (any, error) // функция, выполняющая запрос doFunc func(context.Context) (any, error) // функция, выполняющая запрос
resultCh chan requestResult // канал, через который воркер вернёт результат resultCh chan requestResult // канал, через который воркер вернёт результат
} }
@@ -53,7 +56,7 @@ func newWorkerPool(workers int, queueSize int) *workerPool {
// start запускает воркеры (горутины), которые будут обрабатывать задачи из очереди. // start запускает воркеры (горутины), которые будут обрабатывать задачи из очереди.
// Метод идемпотентен: если пул уже запущен — ничего не делает. // Метод идемпотентен: если пул уже запущен — ничего не делает.
// Должен вызываться перед первым вызовом submit. // Должен вызываться перед первым вызовом submit.
func (p *workerPool) start(ctx context.Context) { func (p *workerPool) start() {
p.startedMu.Lock() p.startedMu.Lock()
defer p.startedMu.Unlock() defer p.startedMu.Unlock()
if p.started { if p.started {
@@ -64,7 +67,7 @@ func (p *workerPool) start(ctx context.Context) {
// Запускаем воркеры — каждый будет обрабатывать задачи в бесконечном цикле // Запускаем воркеры — каждый будет обрабатывать задачи в бесконечном цикле
for i := 0; i < p.workers; i++ { for i := 0; i < p.workers; i++ {
p.wg.Add(1) p.wg.Add(1)
go p.worker(ctx) // запускаем горутину с контекстом go p.worker() // запускаем горутину
} }
} }
@@ -72,8 +75,15 @@ func (p *workerPool) start(ctx context.Context) {
// Отправляет сигнал остановки через quit-канал и ждёт завершения всех активных задач. // Отправляет сигнал остановки через quit-канал и ждёт завершения всех активных задач.
// Безопасно вызывать многократно — после остановки повторные вызовы не имеют эффекта. // Безопасно вызывать многократно — после остановки повторные вызовы не имеют эффекта.
func (p *workerPool) stop() { func (p *workerPool) stop() {
p.stopOnce.Do(func() {
p.startedMu.Lock()
p.stopped = true
p.started = false
close(p.quit) // сигнал для всех воркеров — выйти из цикла close(p.quit) // сигнал для всех воркеров — выйти из цикла
p.startedMu.Unlock()
p.wg.Wait() // ждём, пока все воркеры завершатся p.wg.Wait() // ждём, пока все воркеры завершатся
})
} }
// submit отправляет задачу в очередь и возвращает канал, через который будет получен результат. // submit отправляет задачу в очередь и возвращает канал, через который будет получен результат.
@@ -81,8 +91,15 @@ func (p *workerPool) stop() {
// Канал результата имеет буфер 1, чтобы не блокировать воркера при записи. // Канал результата имеет буфер 1, чтобы не блокировать воркера при записи.
// Контекст используется для отмены задачи, если клиент отменил запрос до отправки. // Контекст используется для отмены задачи, если клиент отменил запрос до отправки.
func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any, error)) (<-chan requestResult, error) { func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any, error)) (<-chan requestResult, error) {
p.startedMu.Lock()
if p.stopped || !p.started {
p.startedMu.Unlock()
return nil, ErrPoolStopped
}
// Проверяем, не превышена ли очередь // Проверяем, не превышена ли очередь
if len(p.taskCh) >= p.queueSize { if len(p.taskCh) >= p.queueSize {
p.startedMu.Unlock()
return nil, ErrPoolQueueFull return nil, ErrPoolQueueFull
} }
@@ -91,6 +108,7 @@ func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any,
// Создаём обёртку задачи // Создаём обёртку задачи
envelope := requestEnvelope{ envelope := requestEnvelope{
ctx: ctx,
doFunc: do, doFunc: do,
resultCh: resultCh, resultCh: resultCh,
} }
@@ -98,12 +116,15 @@ func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any,
// Пытаемся отправить задачу в очередь // Пытаемся отправить задачу в очередь
select { select {
case <-ctx.Done(): case <-ctx.Done():
p.startedMu.Unlock()
// Клиент отменил операцию до отправки — возвращаем ошибку отмены // Клиент отменил операцию до отправки — возвращаем ошибку отмены
return nil, ctx.Err() return nil, ctx.Err()
case p.taskCh <- envelope: case p.taskCh <- envelope:
p.startedMu.Unlock()
// Успешно отправлено — возвращаем канал для чтения результата // Успешно отправлено — возвращаем канал для чтения результата
return resultCh, nil return resultCh, nil
default: default:
p.startedMu.Unlock()
// Очередь переполнена — не должно происходить при проверке len(p.taskCh), но на всякий случай // Очередь переполнена — не должно происходить при проверке len(p.taskCh), но на всякий случай
return nil, ErrPoolQueueFull return nil, ErrPoolQueueFull
} }
@@ -117,18 +138,32 @@ func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any,
// - закрывает канал, чтобы клиент мог прочитать и завершить // - закрывает канал, чтобы клиент мог прочитать и завершить
// //
// После закрытия quit-канала — воркер завершает работу. // После закрытия quit-канала — воркер завершает работу.
func (p *workerPool) worker(ctx context.Context) { func (p *workerPool) worker() {
defer p.wg.Done() // уменьшаем WaitGroup при завершении горутины defer p.wg.Done() // уменьшаем WaitGroup при завершении горутины
for { for {
select { select {
case <-p.quit: case <-p.quit:
// Получен сигнал остановки — выходим из цикла // Получен сигнал остановки — дренируем очередь и выходим.
// После stop() новые задачи не принимаются.
for {
select {
case envelope := <-p.taskCh:
p.executeEnvelope(envelope)
default:
return return
}
}
case envelope := <-p.taskCh: case envelope := <-p.taskCh:
p.executeEnvelope(envelope)
}
}
}
func (p *workerPool) executeEnvelope(envelope requestEnvelope) {
// Выполняем задачу с переданным контекстом (клиентский или общий) // Выполняем задачу с переданным контекстом (клиентский или общий)
value, err := envelope.doFunc(ctx) value, err := envelope.doFunc(envelope.ctx)
// Записываем результат в канал — не блокируем, т.к. буфер 1 // Записываем результат в канал — не блокируем, т.к. буфер 1
envelope.resultCh <- requestResult{ envelope.resultCh <- requestResult{
@@ -137,6 +172,4 @@ func (p *workerPool) worker(ctx context.Context) {
} }
// Закрываем канал — клиент знает, что результат пришёл и больше не будет // Закрываем канал — клиент знает, что результат пришёл и больше не будет
close(envelope.resultCh) close(envelope.resultCh)
}
}
} }

53
tgapi/stars_methods.go Normal file
View File

@@ -0,0 +1,53 @@
package tgapi
// GetStarTransactionsP holds parameters for the getStarTransactions method.
// See https://core.telegram.org/bots/api#getstartransactions
type GetStarTransactionsP struct {
Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"`
}
// GetMyStarBalance returns the bot's Telegram Star balance.
// See https://core.telegram.org/bots/api#getmystarbalance
func (api *API) GetMyStarBalance() (StarAmount, error) {
req := NewRequest[StarAmount]("getMyStarBalance", NoParams)
return req.Do(api)
}
// GetStarTransactions returns Telegram Star transactions for the bot.
// See https://core.telegram.org/bots/api#getstartransactions
func (api *API) GetStarTransactions(params GetStarTransactionsP) (StarTransactions, error) {
req := NewRequest[StarTransactions]("getStarTransactions", params)
return req.Do(api)
}
// RefundStarPaymentP holds parameters for the refundStarPayment method.
// See https://core.telegram.org/bots/api#refundstarpayment
type RefundStarPaymentP struct {
UserID int64 `json:"user_id"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
}
// RefundStarPayment refunds a successful Telegram Stars payment.
// Returns true on success.
// See https://core.telegram.org/bots/api#refundstarpayment
func (api *API) RefundStarPayment(params RefundStarPaymentP) (bool, error) {
req := NewRequest[bool]("refundStarPayment", params)
return req.Do(api)
}
// EditUserStarSubscriptionP holds parameters for the editUserStarSubscription method.
// See https://core.telegram.org/bots/api#edituserstarsubscription
type EditUserStarSubscriptionP struct {
UserID int64 `json:"user_id"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
IsCanceled bool `json:"is_canceled"`
}
// EditUserStarSubscription cancels or re-enables a user star subscription extension.
// Returns true on success.
// See https://core.telegram.org/bots/api#edituserstarsubscription
func (api *API) EditUserStarSubscription(params EditUserStarSubscriptionP) (bool, error) {
req := NewRequest[bool]("editUserStarSubscription", params)
return req.Do(api)
}

18
tgapi/stars_types.go Normal file
View File

@@ -0,0 +1,18 @@
package tgapi
// StarTransaction describes a Telegram Star transaction.
// See https://core.telegram.org/bots/api#startransaction
type StarTransaction struct {
ID string `json:"id"`
Amount int `json:"amount"`
NanostarAmount int `json:"nanostar_amount,omitempty"`
Date int `json:"date"`
Source map[string]any `json:"source,omitempty"`
Receiver map[string]any `json:"receiver,omitempty"`
}
// StarTransactions contains a list of Telegram Star transactions.
// See https://core.telegram.org/bots/api#startransactions
type StarTransactions struct {
Transactions []StarTransaction `json:"transactions"`
}

View File

@@ -49,10 +49,29 @@ func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickersP) ([]Sticke
return req.Do(api) return req.Do(api)
} }
// UploadStickerFileP holds parameters for the uploadStickerFile method.
// See https://core.telegram.org/bots/api#uploadstickerfile
type UploadStickerFileP struct {
UserID int64 `json:"user_id"`
StickerFormat InputStickerFormat `json:"sticker_format"`
}
// UploadStickerFile uploads a sticker file for later use in sticker set methods.
// sticker is the file to upload.
// See https://core.telegram.org/bots/api#uploadstickerfile
func (api *API) UploadStickerFile(params UploadStickerFileP, sticker UploaderFile) (File, error) {
uploader := NewUploader(api)
defer func() {
_ = uploader.Close()
}()
req := NewUploaderRequest[File]("uploadStickerFile", params, sticker.SetType(UploaderStickerType))
return req.Do(uploader)
}
// CreateNewStickerSetP holds parameters for the createNewStickerSet method. // CreateNewStickerSetP holds parameters for the createNewStickerSet method.
// See https://core.telegram.org/bots/api#createnewstickerset // See https://core.telegram.org/bots/api#createnewstickerset
type CreateNewStickerSetP struct { type CreateNewStickerSetP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
Title string `json:"title"` Title string `json:"title"`
@@ -72,7 +91,7 @@ func (api *API) CreateNewStickerSet(params CreateNewStickerSetP) (bool, error) {
// AddStickerToSetP holds parameters for the addStickerToSet method. // AddStickerToSetP holds parameters for the addStickerToSet method.
// See https://core.telegram.org/bots/api#addstickertoset // See https://core.telegram.org/bots/api#addstickertoset
type AddStickerToSetP struct { type AddStickerToSetP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
Sticker InputSticker `json:"sticker"` Sticker InputSticker `json:"sticker"`
} }
@@ -117,7 +136,7 @@ func (api *API) DeleteStickerFromSet(params DeleteStickerFromSetP) (bool, error)
// ReplaceStickerInSetP holds parameters for the replaceStickerInSet method. // ReplaceStickerInSetP holds parameters for the replaceStickerInSet method.
// See https://core.telegram.org/bots/api#replacestickerinset // See https://core.telegram.org/bots/api#replacestickerinset
type ReplaceStickerInSetP struct { type ReplaceStickerInSetP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
OldSticker string `json:"old_sticker"` OldSticker string `json:"old_sticker"`
Sticker InputSticker `json:"sticker"` Sticker InputSticker `json:"sticker"`
@@ -195,7 +214,7 @@ func (api *API) SetStickerSetTitle(params SetStickerSetTitleP) (bool, error) {
// See https://core.telegram.org/bots/api#setstickersetthumbnail // See https://core.telegram.org/bots/api#setstickersetthumbnail
type SetStickerSetThumbnailP struct { type SetStickerSetThumbnailP struct {
Name string `json:"name"` Name string `json:"name"`
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Thumbnail string `json:"thumbnail"` Thumbnail string `json:"thumbnail"`
Format InputStickerFormat `json:"format"` Format InputStickerFormat `json:"format"`
} }
@@ -218,9 +237,7 @@ type SetCustomEmojiStickerSetThumbnailP struct {
// SetCustomEmojiStickerSetThumbnail sets the thumbnail of a custom emoji sticker set. // SetCustomEmojiStickerSetThumbnail sets the thumbnail of a custom emoji sticker set.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail // See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
// func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSetThumbnailP) (bool, error) {
// Note: This method uses SetStickerSetThumbnailP as its parameter type, which might be inconsistent.
func (api *API) SetCustomEmojiStickerSetThumbnail(params SetStickerSetThumbnailP) (bool, error) {
req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params) req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params)
return req.Do(api) return req.Do(api)
} }

View File

@@ -52,7 +52,7 @@ type Sticker struct {
MaskPosition *MaskPosition `json:"mask_position,omitempty"` MaskPosition *MaskPosition `json:"mask_position,omitempty"`
CustomEmojiID *string `json:"custom_emoji_id,omitempty"` CustomEmojiID *string `json:"custom_emoji_id,omitempty"`
NeedRepainting *bool `json:"need_repainting,omitempty"` NeedRepainting *bool `json:"need_repainting,omitempty"`
FileSize *int `json:"file_size,omitempty"` FileSize *int64 `json:"file_size,omitempty"`
} }
// StickerSet represents a sticker set. // StickerSet represents a sticker set.

View File

@@ -160,7 +160,7 @@ type PaidMediaPurchased struct {
type File struct { type File struct {
FileId string `json:"file_id"` FileId string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"` FileUniqueID string `json:"file_unique_id"`
FileSize int `json:"file_size,omitempty"` FileSize int64 `json:"file_size,omitempty"`
FilePath string `json:"file_path,omitempty"` FilePath string `json:"file_path,omitempty"`
} }
@@ -175,7 +175,7 @@ type Audio struct {
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
MimeType string `json:"mime_type,omitempty"` MimeType string `json:"mime_type,omitempty"`
FileSize int `json:"file_size,omitempty"` FileSize int64 `json:"file_size,omitempty"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"` Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
} }
@@ -234,7 +234,7 @@ type ChatMemberUpdated struct {
type ChatJoinRequest struct { type ChatJoinRequest struct {
Chat Chat `json:"chat"` Chat Chat `json:"chat"`
From User `json:"from"` From User `json:"from"`
UserChatID int `json:"user_chat_id"` UserChatID int64 `json:"user_chat_id"`
Date int64 `json:"date"` Date int64 `json:"date"`
Bio *string `json:"bio,omitempty"` Bio *string `json:"bio,omitempty"`
InviteLink *ChatInviteLink `json:"invite_link,omitempty"` InviteLink *ChatInviteLink `json:"invite_link,omitempty"`

View File

@@ -14,13 +14,22 @@ import (
) )
const ( const (
// UploaderPhotoType is the multipart field name for photo uploads.
UploaderPhotoType UploaderFileType = "photo" UploaderPhotoType UploaderFileType = "photo"
// UploaderVideoType is the multipart field name for video uploads.
UploaderVideoType UploaderFileType = "video" UploaderVideoType UploaderFileType = "video"
// UploaderAudioType is the multipart field name for audio uploads.
UploaderAudioType UploaderFileType = "audio" UploaderAudioType UploaderFileType = "audio"
// UploaderDocumentType is the multipart field name for document uploads.
UploaderDocumentType UploaderFileType = "document" UploaderDocumentType UploaderFileType = "document"
// UploaderVoiceType is the multipart field name for voice uploads.
UploaderVoiceType UploaderFileType = "voice" UploaderVoiceType UploaderFileType = "voice"
// UploaderVideoNoteType is the multipart field name for video-note uploads.
UploaderVideoNoteType UploaderFileType = "video_note" UploaderVideoNoteType UploaderFileType = "video_note"
// UploaderThumbnailType is the multipart field name for thumbnail uploads.
UploaderThumbnailType UploaderFileType = "thumbnail" UploaderThumbnailType UploaderFileType = "thumbnail"
// UploaderStickerType is the multipart field name for sticker uploads.
UploaderStickerType UploaderFileType = "sticker"
) )
// UploaderFileType represents the Telegram form field name for a file upload. // UploaderFileType represents the Telegram form field name for a file upload.
@@ -40,24 +49,35 @@ func NewUploaderFile(name string, data []byte) UploaderFile {
return UploaderFile{filename: name, data: data, field: t} return UploaderFile{filename: name, data: data, field: t}
} }
// SetType used when auto-detect failed. // SetType overrides the auto-detected upload field type.
// i.e. you sending a voice message, but it detects as audio, or if you send audio with thumbnail // For example, use it when a voice file is detected as audio.
func (f UploaderFile) SetType(t UploaderFileType) UploaderFile { func (f UploaderFile) SetType(t UploaderFileType) UploaderFile {
f.field = t f.field = t
return f return f
} }
// Uploader is a Telegram Bot API client specialized for multipart file uploads.
//
// Use Uploader methods when you need to upload binary files directly
// (InputFile/multipart). For JSON-only calls (file_id, URL, plain params), use API.
type Uploader struct { type Uploader struct {
api *API api *API
logger *slog.Logger logger *slog.Logger
} }
// NewUploader creates a multipart uploader bound to an API client.
func NewUploader(api *API) *Uploader { func NewUploader(api *API) *Uploader {
logger := slog.CreateLogger().Level(utils.GetLoggerLevel()).Prefix("UPLOADER") logger := slog.CreateLogger().Level(utils.GetLoggerLevel()).Prefix("UPLOADER")
logger.AddWriter(logger.CreateJsonStdoutWriter()) logger.AddWriter(logger.CreateJsonStdoutWriter())
return &Uploader{api, logger} return &Uploader{api, logger}
} }
// Close flushes and closes uploader logger resources.
// See https://core.telegram.org/bots/api
func (u *Uploader) Close() error { return u.logger.Close() } func (u *Uploader) Close() error { return u.logger.Close() }
// GetLogger returns uploader logger instance.
// See https://core.telegram.org/bots/api
func (u *Uploader) GetLogger() *slog.Logger { return u.logger } func (u *Uploader) GetLogger() *slog.Logger { return u.logger }
// UploaderRequest is a multipart file upload request to the Telegram API. // UploaderRequest is a multipart file upload request to the Telegram API.
@@ -90,16 +110,10 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
for { for {
if up.api.Limiter != nil { if up.api.Limiter != nil {
if up.api.dropOverflowLimit { if err := up.api.Limiter.Check(ctx, up.api.dropOverflowLimit, r.chatId); err != nil {
if !up.api.Limiter.GlobalAllow() {
return zero, utils.ErrDropOverflow
}
} else {
if err := up.api.Limiter.GlobalWait(ctx); err != nil {
return zero, err return zero, err
} }
} }
}
buf, contentType, err := prepareMultipart(r.files, r.params) buf, contentType, err := prepareMultipart(r.files, r.params)
if err != nil { if err != nil {

View File

@@ -24,10 +24,10 @@ type UploadPhotoP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadPhoto uploads a photo and sends it as a message. // SendPhoto uploads a photo via multipart and sends it as a message.
// file is the photo file to upload. // file is the photo file to upload.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
func (u *Uploader) UploadPhoto(params UploadPhotoP, file UploaderFile) (Message, error) { func (u *Uploader) SendPhoto(params UploadPhotoP, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file) req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file)
return req.Do(u) return req.Do(u)
} }
@@ -58,10 +58,10 @@ type UploadAudioP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadAudio uploads an audio file and sends it as a message. // SendAudio uploads an audio file via multipart and sends it as a message.
// files are the audio file(s) to upload (typically one file). // files are the audio file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
func (u *Uploader) UploadAudio(params UploadAudioP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAudio(params UploadAudioP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -89,11 +89,11 @@ type UploadDocumentP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadDocument uploads a document and sends it as a message. // SendDocument uploads a document via multipart and sends it as a message.
// files are the document file(s) to upload (typically one file). // files are the document file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
func (u *Uploader) UploadDocument(params UploadDocumentP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendDocument(params UploadDocumentP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequest[Message]("sendDocument", params, files...) req := NewUploaderRequestWithChatID[Message]("sendDocument", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -127,11 +127,11 @@ type UploadVideoP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadVideo uploads a video and sends it as a message. // SendVideo uploads a video via multipart and sends it as a message.
// files are the video file(s) to upload (typically one file). // files are the video file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
func (u *Uploader) UploadVideo(params UploadVideoP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideo(params UploadVideoP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequest[Message]("sendVideo", params, files...) req := NewUploaderRequestWithChatID[Message]("sendVideo", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -163,11 +163,11 @@ type UploadAnimationP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadAnimation uploads an animation (GIF or H.264/MPEG-4 AVC video without sound) and sends it as a message. // SendAnimation uploads an animation via multipart and sends it as a message.
// files are the animation file(s) to upload (typically one file). // files are the animation file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
func (u *Uploader) UploadAnimation(params UploadAnimationP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAnimation(params UploadAnimationP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequest[Message]("sendAnimation", params, files...) req := NewUploaderRequestWithChatID[Message]("sendAnimation", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -194,11 +194,11 @@ type UploadVoiceP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadVoice uploads a voice note and sends it as a message. // SendVoice uploads a voice note via multipart and sends it as a message.
// files are the voice file(s) to upload (typically one file). // files are the voice file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
func (u *Uploader) UploadVoice(params UploadVoiceP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVoice(params UploadVoiceP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequest[Message]("sendVoice", params, files...) req := NewUploaderRequestWithChatID[Message]("sendVoice", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -223,11 +223,11 @@ type UploadVideoNoteP struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
} }
// UploadVideoNote uploads a video note (rounded video) and sends it as a message. // SendVideoNote uploads a video note via multipart and sends it as a message.
// files are the video note file(s) to upload (typically one file). // files are the video note file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
func (u *Uploader) UploadVideoNote(params UploadVideoNoteP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideoNote(params UploadVideoNoteP, files ...UploaderFile) (Message, error) {
req := NewUploaderRequest[Message]("sendVideoNote", params, files...) req := NewUploaderRequestWithChatID[Message]("sendVideoNote", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -237,10 +237,10 @@ type UploadChatPhotoP struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// UploadChatPhoto uploads a new chat photo. // SetChatPhoto uploads a new chat photo.
// photo is the photo file to upload. // photo is the photo file to upload.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
func (u *Uploader) UploadChatPhoto(params UploadChatPhotoP, photo UploaderFile) (Message, error) { func (u *Uploader) SetChatPhoto(params UploadChatPhotoP, photo UploaderFile) (bool, error) {
req := NewUploaderRequest[Message]("sendChatPhoto", params, photo) req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo)
return req.Do(u) return req.Do(u)
} }

View File

@@ -3,7 +3,7 @@ package tgapi
// GetUserProfilePhotosP holds parameters for the GetUserProfilePhotos method. // GetUserProfilePhotosP holds parameters for the GetUserProfilePhotos method.
// See https://core.telegram.org/bots/api#getuserprofilephotos // See https://core.telegram.org/bots/api#getuserprofilephotos
type GetUserProfilePhotosP struct { type GetUserProfilePhotosP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
} }
@@ -18,7 +18,7 @@ func (api *API) GetUserProfilePhotos(params GetUserProfilePhotosP) (UserProfileP
// GetUserProfileAudiosP holds parameters for the GetUserProfileAudios method. // GetUserProfileAudiosP holds parameters for the GetUserProfileAudios method.
// See https://core.telegram.org/bots/api#getuserprofileaudios // See https://core.telegram.org/bots/api#getuserprofileaudios
type GetUserProfileAudiosP struct { type GetUserProfileAudiosP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
} }
@@ -33,7 +33,7 @@ func (api *API) GetUserProfileAudios(params GetUserProfileAudiosP) (UserProfileA
// SetUserEmojiStatusP holds parameters for the SetUserEmojiStatus method. // SetUserEmojiStatusP holds parameters for the SetUserEmojiStatus method.
// See https://core.telegram.org/bots/api#setuseremojistatus // See https://core.telegram.org/bots/api#setuseremojistatus
type SetUserEmojiStatusP struct { type SetUserEmojiStatusP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
EmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` EmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
ExpirationDate int `json:"emoji_status_expiration_date,omitempty"` ExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
} }
@@ -49,7 +49,7 @@ func (api *API) SetUserEmojiStatus(params SetUserEmojiStatusP) (bool, error) {
// GetUserGiftsP holds parameters for the GetUserGifts method. // GetUserGiftsP holds parameters for the GetUserGifts method.
// See https://core.telegram.org/bots/api#getusergifts // See https://core.telegram.org/bots/api#getusergifts
type GetUserGiftsP struct { type GetUserGiftsP struct {
UserID int `json:"user_id"` UserID int64 `json:"user_id"`
ExcludeUnlimited bool `json:"exclude_unlimited,omitempty"` ExcludeUnlimited bool `json:"exclude_unlimited,omitempty"`
ExcludeLimitedUpgradable bool `json:"exclude_limited_upgradable,omitempty"` ExcludeLimitedUpgradable bool `json:"exclude_limited_upgradable,omitempty"`
ExcludeLimitedNonUpgradable bool `json:"exclude_limited_non_upgradable,omitempty"` ExcludeLimitedNonUpgradable bool `json:"exclude_limited_non_upgradable,omitempty"`

View File

@@ -3,7 +3,7 @@ package tgapi
// User represents a Telegram user or bot. // User represents a Telegram user or bot.
// See https://core.telegram.org/bots/api#user // See https://core.telegram.org/bots/api#user
type User struct { type User struct {
ID int `json:"id"` ID int64 `json:"id"`
IsBot bool `json:"is_bot"` IsBot bool `json:"is_bot"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName *string `json:"last_name,omitempty"` LastName *string `json:"last_name,omitempty"`

View File

@@ -6,7 +6,10 @@ import (
"git.nix13.pw/scuroneko/laniakea/utils" "git.nix13.pw/scuroneko/laniakea/utils"
) )
// Ptr returns a pointer to v.
func Ptr[T any](v T) *T { return &v } func Ptr[T any](v T) *T { return &v }
// Val returns dereferenced pointer value or def when p is nil.
func Val[T any](p *T, def T) T { func Val[T any](p *T, def T) T {
if p != nil { if p != nil {
return *p return *p
@@ -14,8 +17,8 @@ func Val[T any](p *T, def T) T {
return def return def
} }
// EscapeMarkdown // EscapeMarkdown escapes special characters for legacy Telegram Markdown.
// Deprecated. Use MarkdownV2 // Deprecated: Use EscapeMarkdownV2.
func EscapeMarkdown(s string) string { func EscapeMarkdown(s string) string {
s = strings.ReplaceAll(s, "_", `\_`) s = strings.ReplaceAll(s, "_", `\_`)
s = strings.ReplaceAll(s, "*", `\*`) s = strings.ReplaceAll(s, "*", `\*`)
@@ -40,6 +43,8 @@ func EscapeMarkdownV2(s string) string {
} }
return s return s
} }
// EscapePunctuation escapes '.', '!' and '-' for MarkdownV2 fragments.
func EscapePunctuation(s string) string { func EscapePunctuation(s string) string {
symbols := []string{".", "!", "-"} symbols := []string{".", "!", "-"}
for _, symbol := range symbols { for _, symbol := range symbols {
@@ -48,6 +53,7 @@ func EscapePunctuation(s string) string {
return s return s
} }
// Version constants mirror values from the internal utils/version package.
const ( const (
VersionString = utils.VersionString VersionString = utils.VersionString
VersionMajor = utils.VersionMajor VersionMajor = utils.VersionMajor

View File

@@ -36,6 +36,17 @@ func NewRateLimiter() *RateLimiter {
} }
} }
// SetGlobalRate overrides global request-per-second limit and burst.
// If rps <= 0, current settings are kept.
func (rl *RateLimiter) SetGlobalRate(rps int) {
if rps <= 0 {
return
}
rl.globalMu.Lock()
defer rl.globalMu.Unlock()
rl.globalLimiter = rate.NewLimiter(rate.Limit(rps), rps)
}
// SetGlobalLock sets a global cooldown period (e.g., after receiving 429 from Telegram). // SetGlobalLock sets a global cooldown period (e.g., after receiving 429 from Telegram).
// If retryAfter <= 0, no lock is applied. // If retryAfter <= 0, no lock is applied.
func (rl *RateLimiter) SetGlobalLock(retryAfter int) { func (rl *RateLimiter) SetGlobalLock(retryAfter int) {
@@ -64,7 +75,11 @@ func (rl *RateLimiter) GlobalWait(ctx context.Context) error {
if err := rl.waitForGlobalUnlock(ctx); err != nil { if err := rl.waitForGlobalUnlock(ctx); err != nil {
return err return err
} }
return rl.globalLimiter.Wait(ctx) limiter := rl.getGlobalLimiter()
if limiter == nil {
return nil
}
return limiter.Wait(ctx)
} }
// Wait blocks until a request for the given chat can be made. // Wait blocks until a request for the given chat can be made.
@@ -77,8 +92,21 @@ func (rl *RateLimiter) Wait(ctx context.Context, chatID int64) error {
if err := rl.waitForGlobalUnlock(ctx); err != nil { if err := rl.waitForGlobalUnlock(ctx); err != nil {
return err return err
} }
limiter := rl.getChatLimiter(chatID) limiter := rl.getGlobalLimiter()
return limiter.Wait(ctx) if limiter != nil {
if err := limiter.Wait(ctx); err != nil {
return err
}
}
chatLimiter := rl.getChatLimiter(chatID)
return chatLimiter.Wait(ctx)
}
// getGlobalLimiter returns the global limiter safely under read lock.
func (rl *RateLimiter) getGlobalLimiter() *rate.Limiter {
rl.globalMu.RLock()
defer rl.globalMu.RUnlock()
return rl.globalLimiter
} }
// GlobalAllow checks if a global request can be made without blocking. // GlobalAllow checks if a global request can be made without blocking.
@@ -91,7 +119,11 @@ func (rl *RateLimiter) GlobalAllow() bool {
if !until.IsZero() && time.Now().Before(until) { if !until.IsZero() && time.Now().Before(until) {
return false return false
} }
return rl.globalLimiter.Allow() limiter := rl.getGlobalLimiter()
if limiter == nil {
return true
}
return limiter.Allow()
} }
// Allow checks if a request for the given chat can be made without blocking. // Allow checks if a request for the given chat can be made without blocking.
@@ -115,13 +147,14 @@ func (rl *RateLimiter) Allow(chatID int64) bool {
} }
// Check global token bucket // Check global token bucket
if !rl.globalLimiter.Allow() { limiter := rl.getGlobalLimiter()
if limiter != nil && !limiter.Allow() {
return false return false
} }
// Check chat token bucket // Check chat token bucket
limiter := rl.getChatLimiter(chatID) chatLimiter := rl.getChatLimiter(chatID)
return limiter.Allow() return chatLimiter.Allow()
} }
// Check applies rate limiting based on configuration. // Check applies rate limiting based on configuration.

View File

@@ -10,6 +10,7 @@ import (
"strings" "strings"
) )
// Encode writes struct fields into multipart form-data using json tags as field names.
func Encode[T any](w *multipart.Writer, req T) error { func Encode[T any](w *multipart.Writer, req T) error {
v := reflect.ValueOf(req) v := reflect.ValueOf(req)
if v.Kind() == reflect.Ptr { if v.Kind() == reflect.Ptr {

View File

@@ -6,6 +6,7 @@ import (
"git.nix13.pw/scuroneko/slog" "git.nix13.pw/scuroneko/slog"
) )
// GetLoggerLevel returns DEBUG when DEBUG=true in env, otherwise FATAL.
func GetLoggerLevel() slog.LogLevel { func GetLoggerLevel() slog.LogLevel {
level := slog.FATAL level := slog.FATAL
if os.Getenv("DEBUG") == "true" { if os.Getenv("DEBUG") == "true" {

View File

@@ -1,9 +1,9 @@
package utils package utils
const ( const (
VersionString = "1.0.0-beta.21" VersionString = "1.0.0-beta.22"
VersionMajor = 1 VersionMajor = 1
VersionMinor = 0 VersionMinor = 0
VersionPatch = 0 VersionPatch = 0
VersionBeta = 21 VersionBeta = 22
) )