v1.0.0 beta 19
This commit is contained in:
@@ -61,23 +61,22 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
|
|||||||
usage := fmt.Sprintf("Usage: /%s %s", cmd.command, strings.Join(descArgs, " "))
|
usage := fmt.Sprintf("Usage: /%s %s", cmd.command, strings.Join(descArgs, " "))
|
||||||
if desc != "" {
|
if desc != "" {
|
||||||
desc = fmt.Sprintf("%s. %s", desc, usage)
|
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]+
|
// checkCmdRegex check if command satisfy regexp [a-zA-Z0-9]+
|
||||||
// Return true if satisfy, else false.
|
// Return true if satisfied, else false.
|
||||||
func checkCmdRegex(cmd string) bool {
|
func checkCmdRegex(cmd string) bool { return CmdRegexp.MatchString(cmd) }
|
||||||
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.
|
// and converts them into tgapi.BotCommand objects.
|
||||||
//
|
//
|
||||||
// Commands marked with skipAutoCmd = true are excluded from auto-registration.
|
// Commands marked with skipAutoCmd = true are excluded from auto-registration.
|
||||||
// This allows plugins to opt out of automatic command generation (e.g., for
|
// This allows plugins to opt out of automatic command generation (e.g., for
|
||||||
// internal or hidden commands).
|
// 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)
|
commands := make([]tgapi.BotCommand, 0)
|
||||||
for _, cmd := range pl.commands {
|
for _, cmd := range pl.commands {
|
||||||
if cmd.skipAutoCmd {
|
if cmd.skipAutoCmd {
|
||||||
@@ -91,6 +90,21 @@ func generateBotCommandForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
|
|||||||
return commands
|
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
|
// AutoGenerateCommands registers all plugin-defined commands with Telegram's Bot API
|
||||||
// across three scopes:
|
// across three scopes:
|
||||||
// - Private chats (users)
|
// - Private chats (users)
|
||||||
@@ -120,17 +134,7 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
|
|||||||
return fmt.Errorf("failed to delete existing commands: %w", err)
|
return fmt.Errorf("failed to delete existing commands: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all non-skipped commands from all plugins
|
commands := gatherCommands(bot)
|
||||||
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
|
|
||||||
if len(commands) > 100 {
|
if len(commands) > 100 {
|
||||||
return ErrTooManyCommands
|
return ErrTooManyCommands
|
||||||
}
|
}
|
||||||
@@ -154,3 +158,39 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
52
drafts.go
52
drafts.go
@@ -29,6 +29,7 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -36,6 +37,8 @@ import (
|
|||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
type draftIdGenerator interface {
|
type draftIdGenerator interface {
|
||||||
// Next returns the next unique draft ID.
|
// Next returns the next unique draft ID.
|
||||||
@@ -73,12 +76,6 @@ type DraftProvider struct {
|
|||||||
api *tgapi.API
|
api *tgapi.API
|
||||||
drafts map[uint64]*Draft
|
drafts map[uint64]*Draft
|
||||||
generator draftIdGenerator
|
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.
|
// 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.
|
// GetDraft retrieves a draft by its ID.
|
||||||
//
|
//
|
||||||
// Returns the draft and true if found, or nil and false if not found.
|
// 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.
|
// After successful flush, each draft is removed from the provider and cleared.
|
||||||
func (p *DraftProvider) FlushAll() error {
|
func (p *DraftProvider) FlushAll() error {
|
||||||
p.mu.RLock()
|
p.mu.Lock()
|
||||||
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.mu.RUnlock()
|
p.drafts = make(map[uint64]*Draft)
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for _, draft := range drafts {
|
for _, draft := range drafts {
|
||||||
@@ -197,18 +167,11 @@ type Draft struct {
|
|||||||
//
|
//
|
||||||
// Panics if chatID is zero — call SetChat() on the provider first.
|
// Panics if chatID is zero — call SetChat() on the provider first.
|
||||||
func (p *DraftProvider) NewDraft(parseMode tgapi.ParseMode) *Draft {
|
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()
|
id := p.generator.Next()
|
||||||
draft := &Draft{
|
draft := &Draft{
|
||||||
api: p.api,
|
api: p.api,
|
||||||
provider: p,
|
provider: p,
|
||||||
chatID: p.chatID,
|
|
||||||
messageThreadID: p.messageThreadID,
|
|
||||||
parseMode: parseMode,
|
parseMode: parseMode,
|
||||||
entities: p.entities, // Shallow copy — caller must ensure immutability
|
|
||||||
ID: id,
|
ID: id,
|
||||||
Message: "",
|
Message: "",
|
||||||
}
|
}
|
||||||
@@ -310,6 +273,9 @@ func (d *Draft) Flush() error {
|
|||||||
|
|
||||||
// push is the internal helper for Push(). It updates the server draft via SendMessageDraft.
|
// push is the internal helper for Push(). It updates the server draft via SendMessageDraft.
|
||||||
func (d *Draft) push(text string) error {
|
func (d *Draft) push(text string) error {
|
||||||
|
if d.chatID == 0 {
|
||||||
|
return ErrDraftChatIDZero
|
||||||
|
}
|
||||||
d.Message += text
|
d.Message += text
|
||||||
params := tgapi.SendMessageDraftP{
|
params := tgapi.SendMessageDraftP{
|
||||||
ChatID: d.chatID,
|
ChatID: d.chatID,
|
||||||
|
|||||||
32
methods.go
32
methods.go
@@ -6,6 +6,38 @@ import (
|
|||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"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) {
|
func (bot *Bot[T]) Updates() ([]tgapi.Update, error) {
|
||||||
offset := bot.GetUpdateOffset()
|
offset := bot.GetUpdateOffset()
|
||||||
params := tgapi.UpdateParams{
|
params := tgapi.UpdateParams{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionString = "1.0.0-beta.18"
|
VersionString = "1.0.0-beta.19"
|
||||||
VersionMajor = 1
|
VersionMajor = 1
|
||||||
VersionMinor = 0
|
VersionMinor = 0
|
||||||
VersionPatch = 0
|
VersionPatch = 0
|
||||||
VersionBeta = 18
|
VersionBeta = 19
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user