From 5976fcd0b8583d0c28bf4e247329175f223129e7 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Fri, 13 Mar 2026 12:25:53 +0300 Subject: [PATCH] v1.0.0 beta 19 --- cmd_generator.go | 76 ++++++++++++++++++++++++++++++++++++------------ drafts.go | 62 +++++++++------------------------------ methods.go | 32 ++++++++++++++++++++ utils/version.go | 4 +-- 4 files changed, 106 insertions(+), 68 deletions(-) diff --git a/cmd_generator.go b/cmd_generator.go index 3f2e0a8..b29de40 100644 --- a/cmd_generator.go +++ b/cmd_generator.go @@ -61,23 +61,22 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand { usage := fmt.Sprintf("Usage: /%s %s", cmd.command, strings.Join(descArgs, " ")) if desc != "" { desc = fmt.Sprintf("%s. %s", desc, usage) + return tgapi.BotCommand{Command: cmd.command, Description: desc} } - return tgapi.BotCommand{Command: cmd.command, Description: desc} + return tgapi.BotCommand{Command: cmd.command, Description: usage} } // checkCmdRegex check if command satisfy regexp [a-zA-Z0-9]+ -// Return true if satisfy, else false. -func checkCmdRegex(cmd string) bool { - return CmdRegexp.MatchString(cmd) -} +// Return true if satisfied, else false. +func checkCmdRegex(cmd string) bool { return CmdRegexp.MatchString(cmd) } -// generateBotCommandForPlugin collects all non-skipped commands from a Plugin[T] +// gatherCommandsForPlugin collects all non-skipped commands from a Plugin[T] // 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 generateBotCommandForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand { +func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand { commands := make([]tgapi.BotCommand, 0) for _, cmd := range pl.commands { if cmd.skipAutoCmd { @@ -91,6 +90,21 @@ func generateBotCommandForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand { return commands } +// gatherCommands collects all commands from all plugins +// and converts them into tgapi.BotCommand objects. +// See gatherCommandsForPlugin +func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand { + commands := make([]tgapi.BotCommand, 0) + for _, pl := range bot.plugins { + if pl.skipAutoCmd { + continue + } + commands = append(commands, gatherCommandsForPlugin(pl)...) + bot.logger.Debugln(fmt.Sprintf("Registered %d commands from plugin %s", len(pl.commands), pl.name)) + } + return commands +} + // AutoGenerateCommands registers all plugin-defined commands with Telegram's Bot API // across three scopes: // - Private chats (users) @@ -120,17 +134,7 @@ func (bot *Bot[T]) AutoGenerateCommands() error { return fmt.Errorf("failed to delete existing commands: %w", err) } - // Collect all non-skipped commands from all plugins - commands := make([]tgapi.BotCommand, 0) - for _, pl := range bot.plugins { - if pl.skipAutoCmd { - continue - } - commands = append(commands, generateBotCommandForPlugin(pl)...) - bot.logger.Debugln(fmt.Sprintf("Registered %d commands from plugin %s", len(pl.commands), pl.name)) - } - - // Enforce Telegram's 100-command limit + commands := gatherCommands(bot) if len(commands) > 100 { return ErrTooManyCommands } @@ -154,3 +158,39 @@ func (bot *Bot[T]) AutoGenerateCommands() error { return nil } + +// AutoGenerateCommandsForScope registers all plugin-defined commands with Telegram's Bot API +// for the specified command scope. It first deletes any existing commands in that scope +// to ensure a clean state, then sets the new set of commands. +// +// The scope parameter defines where the commands should be available (e.g., private chats, +// group chats, chat administrators). See tgapi.BotCommandScope and its predefined types. +// +// Returns ErrTooManyCommands if the total number of commands exceeds 100. +// Returns any API error from Telegram (e.g., network issues, invalid scope). +// +// Usage: +// +// privateScope := &tgapi.BotCommandScope{Type: tgapi.BotCommandScopePrivateType} +// if err := bot.AutoGenerateCommandsForScope(privateScope); err != nil { +// log.Fatal(err) +// } +func (bot *Bot[T]) AutoGenerateCommandsForScope(scope *tgapi.BotCommandScope) error { + _, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommandsP{Scope: scope}) + if err != nil { + return fmt.Errorf("failed to delete existing commands: %w", err) + } + commands := gatherCommands(bot) + if len(commands) > 100 { + return ErrTooManyCommands + } + + _, err = bot.api.SetMyCommands(tgapi.SetMyCommandsP{ + Commands: commands, + Scope: scope, + }) + if err != nil { + return fmt.Errorf("failed to set commands for scope %q: %w", scope.Type, err) + } + return nil +} diff --git a/drafts.go b/drafts.go index 5db19fe..445d3cd 100644 --- a/drafts.go +++ b/drafts.go @@ -29,6 +29,7 @@ package laniakea import ( + "errors" "math/rand/v2" "sync" "sync/atomic" @@ -36,6 +37,8 @@ import ( "git.nix13.pw/scuroneko/laniakea/tgapi" ) +var ErrDraftChatIDZero = errors.New("zero draft chat ID") + // draftIdGenerator defines an interface for generating unique draft IDs. type draftIdGenerator interface { // Next returns the next unique draft ID. @@ -73,12 +76,6 @@ type DraftProvider struct { api *tgapi.API drafts map[uint64]*Draft generator draftIdGenerator - - // Internal defaults — not exposed directly to users. - chatID int64 - messageThreadID int - parseMode tgapi.ParseMode - entities []tgapi.MessageEntity } // NewRandomDraftProvider creates a new DraftProvider using random draft IDs. @@ -109,34 +106,6 @@ func NewLinearDraftProvider(api *tgapi.API, startValue uint64) *DraftProvider { } } -// SetChat sets the target chat and optional message thread for all drafts created -// by this provider. Must be called before NewDraft(). -// -// If not set, NewDraft() will create drafts with zero chatID, which will cause -// SendMessageDraft to fail. Use this method to avoid runtime errors. -func (p *DraftProvider) SetChat(chatID int64, messageThreadID int) *DraftProvider { - p.chatID = chatID - p.messageThreadID = messageThreadID - return p -} - -// SetParseMode sets the default parse mode for all new drafts. -// Overrides the parse mode passed to NewDraft() only if not specified there. -func (p *DraftProvider) SetParseMode(mode tgapi.ParseMode) *DraftProvider { - p.parseMode = mode - return p -} - -// SetEntities sets the default message entities (e.g., bold, links, mentions) -// to be copied into every new draft. -// -// Entities are shallow-copied — if you mutate the slice later, it will affect -// future drafts. For safety, pass a copy if needed. -func (p *DraftProvider) SetEntities(entities []tgapi.MessageEntity) *DraftProvider { - p.entities = entities - return p -} - // GetDraft retrieves a draft by its ID. // // Returns the draft and true if found, or nil and false if not found. @@ -154,12 +123,13 @@ func (p *DraftProvider) GetDraft(id uint64) (*Draft, bool) { // // After successful flush, each draft is removed from the provider and cleared. func (p *DraftProvider) FlushAll() error { - p.mu.RLock() + p.mu.Lock() drafts := make([]*Draft, 0, len(p.drafts)) for _, draft := range p.drafts { drafts = append(drafts, draft) } - p.mu.RUnlock() + p.drafts = make(map[uint64]*Draft) + p.mu.Unlock() var lastErr error for _, draft := range drafts { @@ -197,20 +167,13 @@ type Draft struct { // // Panics if chatID is zero — call SetChat() on the provider first. func (p *DraftProvider) NewDraft(parseMode tgapi.ParseMode) *Draft { - if p.chatID == 0 { - panic("laniakea: DraftProvider.SetChat() must be called before NewDraft()") - } - id := p.generator.Next() draft := &Draft{ - api: p.api, - provider: p, - chatID: p.chatID, - messageThreadID: p.messageThreadID, - parseMode: parseMode, - entities: p.entities, // Shallow copy — caller must ensure immutability - ID: id, - Message: "", + api: p.api, + provider: p, + parseMode: parseMode, + ID: id, + Message: "", } p.mu.Lock() p.drafts[id] = draft @@ -310,6 +273,9 @@ func (d *Draft) Flush() error { // push is the internal helper for Push(). It updates the server draft via SendMessageDraft. func (d *Draft) push(text string) error { + if d.chatID == 0 { + return ErrDraftChatIDZero + } d.Message += text params := tgapi.SendMessageDraftP{ ChatID: d.chatID, diff --git a/methods.go b/methods.go index 21f9e46..f5eb41d 100644 --- a/methods.go +++ b/methods.go @@ -6,6 +6,38 @@ import ( "git.nix13.pw/scuroneko/laniakea/tgapi" ) +// Updates fetches new updates from Telegram API using long polling. +// It respects the bot's current update offset and automatically advances it +// after successful retrieval. The method supports selective update types +// through AllowedUpdates and includes optional request logging. +// +// Parameters: +// - None (uses bot's internal state for offset and allowed updates) +// +// Returns: +// - []tgapi.Update: slice of received updates (empty if none available) +// - error: any error encountered during the API call +// +// Behavior: +// 1. Uses the bot's current update offset (via GetUpdateOffset) +// 2. Requests updates with 30-second timeout +// 3. Filters updates by types specified in bot.GetUpdateTypes() +// 4. Logs raw update JSON if RequestLogger is configured +// 5. Automatically updates the offset to the last received update ID + 1 +// 6. Returns all received updates (empty slice if none) +// +// Note: This is a blocking call that waits up to 30 seconds for new updates. +// For non-blocking behavior, consider using webhooks instead. +// +// Example: +// +// updates, err := bot.Updates() +// if err != nil { +// log.Fatal(err) +// } +// for _, update := range updates { +// // process update +// } func (bot *Bot[T]) Updates() ([]tgapi.Update, error) { offset := bot.GetUpdateOffset() params := tgapi.UpdateParams{ diff --git a/utils/version.go b/utils/version.go index 3bf3187..1ec09b6 100644 --- a/utils/version.go +++ b/utils/version.go @@ -1,9 +1,9 @@ package utils const ( - VersionString = "1.0.0-beta.18" + VersionString = "1.0.0-beta.19" VersionMajor = 1 VersionMinor = 0 VersionPatch = 0 - VersionBeta = 18 + VersionBeta = 19 )