REPOSITORY / ScuroNeko/Laniakea

Compare commits

DIFF REPOSITORY

Compare commits

...
22 changed files with 312 additions and 84 deletions
+14
View File
@@ -0,0 +1,14 @@
version: "2"
run:
timeout: 5m
linters:
disable-all: true
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
issues:
max-issues-per-linter: 0
max-same-issues: 0
+41
View File
@@ -0,0 +1,41 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-yaml
- id: check-json
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: local
hooks:
- id: gofmt
name: gofmt
entry: gofmt -w
language: system
types: [go]
- id: go-vet
name: go vet
entry: go vet ./...
language: system
pass_filenames: false
types: [go]
- id: golangci-lint
name: golangci-lint
entry: golangci-lint run
language: system
pass_filenames: false
types: [go]
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
stages: [pre-push]
types: [go]
-15
View File
@@ -1,15 +0,0 @@
# Проверка наличия golangci-lint
GO_LINT := $(shell command -v golangci-lint 2>/dev/null)
# Цель: запуск всех проверок кода
check:
@echo "🔍 Running code checks..."
@go mod tidy -v
@go vet ./...
@if [ -n "$(GO_LINT)" ]; then \
echo "✅ golangci-lint found, running..." && \
golangci-lint run --timeout=5m --verbose; \
else \
echo "⚠️ golangci-lint not installed. Install with: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.57.2"; \
fi
@go test -race -v ./... 2>/dev/null || echo "⚠️ Tests skipped or failed (run manually with 'go test -race ./...')"
+2
View File
@@ -1,5 +1,7 @@
# Laniakea
![Laniakea](assets/logo.jpg)
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE)
![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases)
+3 -1
View File
@@ -1,10 +1,12 @@
# Laniakea
![Laniakea](assets/logo.jpg)
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE)
![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases)
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке中间件, автоматической генерации команд и встроенному ограничителю скорости запросов.
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру.
[English](README.md)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

+73 -17
View File
@@ -176,28 +176,30 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
// Close gracefully shuts down bot-owned resources.
//
// Closes:
// Close shuts down, in order:
// - Registered plugins via Plugin.Close
// - Uploader (waits for pending uploads)
// - API client
// - API client internals
// - RequestLogger (if enabled)
// - Main logger
//
// RunWithContext does not call Close automatically. The caller is responsible
// for invoking Close after RunWithContext returns to release these resources.
//
// Returns a joined error containing all shutdown failures, if any.
// Close returns a joined error containing all shutdown failures, if any.
func (bot *Bot[T]) Close() error {
var e []error
for _, p := range bot.plugins {
if err := p.Close(); err != nil {
e = append(e, err)
}
}
if err := bot.uploader.Close(); err != nil {
bot.logger.Errorln(err)
e = append(e, err)
}
if _, err := bot.api.Close(); err != nil {
bot.logger.Errorln(err)
e = append(e, err)
}
if err := bot.api.CloseApi(); err != nil {
if err := bot.api.Close(); err != nil {
bot.logger.Errorln(err)
e = append(e, err)
}
@@ -213,6 +215,17 @@ func (bot *Bot[T]) Close() error {
return errors.Join(e...)
}
// CloseRemote sends Telegram Bot API "close" request for the current bot
// instance using ctx for cancellation and deadlines.
//
// This is separate from Bot.Close(), which only releases local resources.
func (bot *Bot[T]) CloseRemote(ctx context.Context) error {
if _, err := bot.api.CloseRemoteWithContext(ctx); err != nil {
return err
}
return nil
}
// initLoggers configures the main and optional request loggers.
//
// Uses DEBUG flag to set log level (DEBUG if true, FATAL otherwise).
@@ -224,27 +237,25 @@ func (bot *Bot[T]) initLoggers(opts *BotOpts) {
level = slog.DEBUG
}
bot.logger = slog.CreateLogger().Level(level).Prefix("BOT")
bot.logger.AddWriter(bot.logger.CreateJsonStdoutWriter())
bot.logger = utils.CreateLogger("BOT", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/main.log", strings.TrimRight(opts.LoggerBasePath, "/"))
fileWriter, err := bot.logger.CreateTextFileWriter(path)
logger, err := utils.CreateFileLogger("BOT", level, path)
if err != nil {
bot.logger.Fatal(err)
}
bot.logger.AddWriter(fileWriter)
bot.logger = logger
}
if opts.UseRequestLogger {
bot.RequestLogger = slog.CreateLogger().Level(level).Prefix("REQUESTS")
bot.RequestLogger.AddWriter(bot.RequestLogger.CreateJsonStdoutWriter())
bot.RequestLogger = utils.CreateLogger("REQUESTS", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/requests.log", strings.TrimRight(opts.LoggerBasePath, "/"))
fileWriter, err := bot.RequestLogger.CreateTextFileWriter(path)
logger, err := utils.CreateFileLogger("REQUESTS", level, path)
if err != nil {
bot.logger.Fatal(err)
}
bot.RequestLogger.AddWriter(fileWriter)
bot.RequestLogger = logger
}
}
}
@@ -273,6 +284,16 @@ func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger }
// Returns nil if not set via DatabaseContext().
func (bot *Bot[T]) GetDBContext() *T { return bot.dbContext }
// GetLoggerLevel returns the effective log level derived from the bot's debug
// flag.
func (bot *Bot[T]) GetLoggerLevel() slog.LogLevel {
level := slog.FATAL
if bot.debug {
level = slog.DEBUG
}
return level
}
// L10n translates a key in the given language.
// Returns empty string if translation not found.
func (bot *Bot[T]) L10n(lang, key string) string {
@@ -334,13 +355,38 @@ func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] {
// Debug enables or disables debug logging.
func (bot *Bot[T]) Debug(debug bool) *Bot[T] {
bot.debug = debug
level := slog.FATAL
if debug {
level = slog.DEBUG
}
bot.logger.Level(level)
if bot.RequestLogger != nil {
bot.RequestLogger.Level(level)
}
for _, p := range bot.plugins {
if p.logger == nil {
continue
}
p.logger.Level(level)
}
return bot
}
// AddPlugins registers one or more plugins.
// Plugins are executed in registration order unless filtered by middleware.
//
// Registration is a commit point for plugin configuration. The Bot stores
// plugin metadata internally, so plugins must be fully configured before they
// are passed here. Post-registration mutation through the original *Plugin is
// not a supported API, even if some changes appear to work due to shared maps.
func (bot *Bot[T]) AddPlugins(plugin ...*Plugin[T]) *Bot[T] {
level := bot.GetLoggerLevel()
for _, p := range plugin {
if p.logger == nil {
logger := utils.CreateLogger(p.name, level)
p.SetLogger(logger)
}
bot.plugins = append(bot.plugins, *p)
bot.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.name))
}
@@ -436,6 +482,11 @@ func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] {
// - Main bot logger
// - Request logger (if enabled)
// - API and Uploader loggers
// - Already registered plugin loggers
//
// Call this after AddPlugins if plugin loggers should also receive the writer.
// Plugins registered later do not automatically inherit previously added
// database writers; call AddDatabaseLoggerWriter again after adding them.
//
// Example:
//
@@ -451,6 +502,11 @@ func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
for _, l := range bot.extraLoggers {
l.AddWriter(w)
}
for _, p := range bot.plugins {
if p.logger != nil {
p.logger.AddWriter(w)
}
}
return bot
}
@@ -476,7 +532,7 @@ func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
// go bot.RunWithContext(ctx)
// // ... later ...
// cancel() // triggers graceful shutdown
// _ = bot.Close()
// _ = bot.Close(context.Background())
func (bot *Bot[T]) RunWithContext(ctx context.Context) {
if len(bot.prefixes) == 0 {
bot.logger.Fatalln("no prefixes defined")
+2 -2
View File
@@ -37,8 +37,8 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
+2
View File
@@ -2,6 +2,8 @@ module git.nix13.pw/scuroneko/laniakea
go 1.26
retract v1.0.0-rc.5
require (
git.nix13.pw/scuroneko/extypes v1.2.2
git.nix13.pw/scuroneko/slog v1.1.2
+9 -1
View File
@@ -22,7 +22,6 @@ func (bot *Bot[T]) handle(u *tgapi.Update) {
ctx := &MsgContext{
Update: *u, Api: bot.api,
botLogger: bot.logger,
errorTemplate: bot.errorTemplate,
l10n: bot.l10n,
draftProvider: bot.draftProvider,
@@ -97,6 +96,11 @@ func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) {
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
return
}
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = bot.logger
}
plugin.executeCmd(cmd, ctx, bot.dbContext)
return
}
@@ -131,6 +135,10 @@ func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) {
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
return
}
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = bot.logger
}
plugin.executePayload(data.Command, ctx, bot.dbContext)
return
}
+23 -20
View File
@@ -19,6 +19,10 @@ type MsgContext struct {
Msg *tgapi.Message
From *tgapi.User
// Logger is the logger assigned by the matched plugin for the current handler call.
// It may fall back to the bot logger when the plugin has no dedicated logger.
Logger *slog.Logger
InlineMsgId string
CallbackMsgId int
CallbackQueryId string
@@ -28,7 +32,6 @@ type MsgContext struct {
Args []string
errorTemplate string
botLogger *slog.Logger
l10n *L10n
draftProvider *DraftProvider
payloadType BotPayloadType
@@ -57,7 +60,7 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard
case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId
default:
ctx.botLogger.Errorln("Can't edit message: no valid message target")
ctx.Logger.Errorln("Can't edit message: no valid message target")
return nil
}
if keyboard != nil {
@@ -65,7 +68,7 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard
}
msg, _, err := ctx.Api.EditMessageText(params)
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
return nil
}
resultMessageID := messageId
@@ -95,7 +98,7 @@ func (m *AnswerMessage) EditMarkdown(text string) *AnswerMessage {
// Supports both regular callback messages and inline callback messages.
func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.CallbackMsgId == 0 && ctx.InlineMsgId == "" {
ctx.botLogger.Errorln("Can't edit non-callback update message")
ctx.Logger.Errorln("Can't edit non-callback update message")
return nil
}
return ctx.edit(ctx.CallbackMsgId, text, keyboard, parseMode)
@@ -139,7 +142,7 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId
default:
ctx.botLogger.Errorln("Can't edit caption: no valid message target")
ctx.Logger.Errorln("Can't edit caption: no valid message target")
return nil
}
if kb != nil {
@@ -148,7 +151,7 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
msg, _, err := ctx.Api.EditMessageCaption(params)
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
return nil
}
resultMessageID := messageId
@@ -188,7 +191,7 @@ func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeybo
// Uses API limiter to respect Telegram rate limits per chat.
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")
ctx.Logger.Errorln("Can't answer message without a message")
return nil
}
params := tgapi.SendMessageP{
@@ -208,7 +211,7 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode t
msg, err := ctx.Api.SendMessage(params)
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
return nil
}
return &AnswerMessage{
@@ -255,7 +258,7 @@ func (ctx *MsgContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *
// answerPhoto sends a photo with optional caption and keyboard.
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")
ctx.Logger.Errorln("Can't answer message without a message")
return nil
}
params := tgapi.SendPhotoP{
@@ -276,7 +279,7 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard, par
msg, err := ctx.Api.SendPhoto(params)
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
return nil
}
return &AnswerMessage{
@@ -323,11 +326,11 @@ func (ctx *MsgContext) AnswerPhotofMarkdown(photoId, template string, args ...an
// delete removes a message by ID.
func (ctx *MsgContext) delete(messageId int) {
if messageId == 0 {
ctx.botLogger.Errorln("Can't delete message: message ID zero")
ctx.Logger.Errorln("Can't delete message: message ID zero")
return
}
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't delete message: no chat message context")
ctx.Logger.Errorln("Can't delete message: no chat message context")
return
}
_, err := ctx.Api.DeleteMessage(tgapi.DeleteMessageP{
@@ -335,7 +338,7 @@ func (ctx *MsgContext) delete(messageId int) {
MessageID: messageId,
})
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
}
}
@@ -345,7 +348,7 @@ func (m *AnswerMessage) Delete() { m.ctx.delete(m.MessageID) }
// CallbackDelete deletes the message that triggered the callback query.
func (ctx *MsgContext) CallbackDelete() {
if ctx.CallbackMsgId == 0 {
ctx.botLogger.Errorln("Can't delete callback message: no callback message ID")
ctx.Logger.Errorln("Can't delete callback message: no callback message ID")
return
}
ctx.delete(ctx.CallbackMsgId)
@@ -362,7 +365,7 @@ func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
Text: text, ShowAlert: showAlert, URL: url,
})
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
}
}
@@ -381,7 +384,7 @@ func (ctx *MsgContext) AnswerCbQueryUrl(u string) { ctx.answerCallbackQuery(u, "
// SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity.
func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't send action without chat message context")
ctx.Logger.Errorln("Can't send action without chat message context")
return
}
params := tgapi.SendChatActionP{
@@ -392,7 +395,7 @@ func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
}
_, err := ctx.Api.SendChatAction(params)
if err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
}
}
@@ -408,7 +411,7 @@ func (ctx *MsgContext) error(err error) {
} else {
ctx.answer(text, nil, tgapi.ParseNone)
}
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
}
// Error is an alias for error().
@@ -416,14 +419,14 @@ func (ctx *MsgContext) Error(err error) { ctx.error(err) }
func (ctx *MsgContext) newDraft(parseMode tgapi.ParseMode) *Draft {
if ctx.Msg == nil {
ctx.botLogger.Errorln("can't create draft: ctx.Msg is nil")
ctx.Logger.Errorln("can't create draft: ctx.Msg is nil")
return nil
}
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ctx.Api.Limiter.Wait(c, ctx.Msg.Chat.ID); err != nil {
ctx.botLogger.Errorln(err)
ctx.Logger.Errorln(err)
return nil
}
+3 -3
View File
@@ -37,8 +37,8 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
@@ -48,7 +48,7 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
Chat: &tgapi.Chat{ID: 42, Type: string(tgapi.ChatTypePrivate)},
DirectMessageTopic: &tgapi.DirectMessageTopic{TopicID: 77},
},
botLogger: slog.CreateLogger(),
Logger: slog.CreateLogger(),
}
answer := ctx.AnswerPhoto("photo-id", "caption")
+59 -2
View File
@@ -5,6 +5,7 @@ import (
"regexp"
"git.nix13.pw/scuroneko/extypes"
"git.nix13.pw/scuroneko/slog"
)
// CommandValueType defines the expected type of a command argument.
@@ -151,19 +152,30 @@ func (c *Command[T]) validateArgs(args []string) error {
// Plugin represents a collection of commands and payloads (e.g., callback handlers),
// with shared middleware and configuration.
//
// A Plugin is intended to be fully configured before it is passed to Bot.AddPlugins.
// After registration, treat the plugin as committed and do not mutate it further.
// Post-registration changes through the original *Plugin are not a supported API.
type Plugin[T DbContext] struct {
name string // Name of the plugin (e.g., "admin", "user")
commands map[string]*Command[T] // Registered commands (triggered by message)
payloads map[string]*Command[T] // Registered payloads (triggered by callback data)
middlewares extypes.Slice[Middleware[T]] // Shared middlewares for all commands/payloads
skipAutoCmd bool // If true, all commands in this plugin are excluded from auto-help
logger *slog.Logger
onClose func() error
}
// NewPlugin creates a new Plugin with the given name.
func NewPlugin[T DbContext](name string) *Plugin[T] {
return &Plugin[T]{
name, make(map[string]*Command[T]),
make(map[string]*Command[T]), extypes.Slice[Middleware[T]]{}, false,
name: name,
commands: make(map[string]*Command[T]),
payloads: make(map[string]*Command[T]),
middlewares: make(extypes.Slice[Middleware[T]], 0),
skipAutoCmd: false,
logger: nil,
}
}
@@ -210,6 +222,51 @@ func (p *Plugin[T]) SkipCommandAutoGen() *Plugin[T] {
return p
}
// SetLogger sets the logger used for this plugin's handlers.
//
// Call this before Bot.AddPlugins. If the plugin is already registered, changing
// the original *Plugin does not update the Bot's internal copy.
func (p *Plugin[T]) SetLogger(l *slog.Logger) *Plugin[T] {
p.logger = l
return p
}
// RemoveLogger clears the custom logger for this plugin.
//
// Call this before Bot.AddPlugins. If the plugin is already registered, changing
// the original *Plugin does not update the Bot's internal copy.
func (p *Plugin[T]) RemoveLogger() *Plugin[T] {
p.logger = nil
return p
}
// SetOnClose registers a callback invoked from Plugin.Close after the plugin
// logger is closed.
//
// Call this before Bot.AddPlugins. If the plugin is already registered, changing
// the original *Plugin does not update the Bot's internal copy.
func (p *Plugin[T]) SetOnClose(f func() error) *Plugin[T] {
p.onClose = f
return p
}
// Close releases plugin-owned resources such as its logger and optional
// OnClose callback.
func (p *Plugin[T]) Close() error {
var e []error
if p.logger != nil {
if err := p.logger.Close(); err != nil {
e = append(e, err)
}
}
if p.onClose != nil {
if err := p.onClose(); err != nil {
e = append(e, err)
}
}
return errors.Join(e...)
}
// executeCmd finds and executes a command by its trigger string.
// Validates arguments and runs middlewares before executor.
// On error, sends an error message to the user via ctx.error().
+4 -5
View File
@@ -95,10 +95,9 @@ type API struct {
}
// NewAPI creates a new API client from options.
// Always call CloseApi() when done to release resources.
// Always call Close() when done to release resources.
func NewAPI(opts *APIOpts) *API {
l := slog.CreateLogger().Level(utils.GetLoggerLevel()).Prefix("API")
l.AddWriter(l.CreateJsonStdoutWriter())
l := utils.CreateLogger("API", utils.GetLoggerLevel())
client := opts.client
if client == nil {
@@ -120,10 +119,10 @@ func NewAPI(opts *APIOpts) *API {
}
}
// CloseApi shuts down the internal worker pool and closes the logger.
// Close shuts down the internal worker pool and closes the logger.
// Must be called to avoid resource leaks.
// See https://core.telegram.org/bots/api
func (api *API) CloseApi() error {
func (api *API) Close() error {
api.pool.stop()
return api.logger.Close()
}
+2 -2
View File
@@ -35,8 +35,8 @@ func TestAPILeavesAcceptEncodingToHTTPTransport(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
+4 -4
View File
@@ -49,18 +49,18 @@ func (api *API) LogOutWithContext(ctx context.Context) (bool, error) {
return req.DoWithContext(ctx, api)
}
// Close closes the bot instance on the local server.
// CloseRemote closes the bot instance on the local server.
// Returns true on success.
// See https://core.telegram.org/bots/api#close
func (api *API) Close() (bool, error) {
func (api *API) CloseRemote() (bool, error) {
req := NewRequest[bool, EmptyParams]("close", NoParams)
return req.Do(api)
}
// CloseWithContext is the context-aware variant of Close.
// CloseRemoteWithContext is the context-aware variant of CloseRemote.
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#close
func (api *API) CloseWithContext(ctx context.Context) (bool, error) {
func (api *API) CloseRemoteWithContext(ctx context.Context) (bool, error) {
req := NewRequest[bool, EmptyParams]("close", NoParams)
return req.DoWithContext(ctx, api)
}
+6 -6
View File
@@ -27,8 +27,8 @@ func TestGetFileByLinkUsesConfiguredAPIURL(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
@@ -60,8 +60,8 @@ func TestGetFileByLinkReturnsHTTPStatusError(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
@@ -97,8 +97,8 @@ func TestGetUpdatesOmitsAllowedUpdatesWhenEmpty(t *testing.T) {
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
+1 -2
View File
@@ -69,8 +69,7 @@ type Uploader struct {
// NewUploader creates a multipart uploader bound to an API client.
func NewUploader(api *API) *Uploader {
logger := slog.CreateLogger().Level(utils.GetLoggerLevel()).Prefix("UPLOADER")
logger.AddWriter(logger.CreateJsonStdoutWriter())
logger := utils.CreateLogger("UPLOADER", utils.GetLoggerLevel())
return &Uploader{api, logger}
}
+2 -2
View File
@@ -44,8 +44,8 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
SetHTTPClient(client),
)
defer func() {
if err := api.CloseApi(); err != nil {
t.Fatalf("CloseApi returned error: %v", err)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
+26
View File
@@ -14,3 +14,29 @@ func GetLoggerLevel() slog.LogLevel {
}
return level
}
// CreateLogger creates a logger with the shared default policy:
// JSON stdout output, provided prefix, and provided level.
func CreateLogger(prefix string, level slog.LogLevel) *slog.Logger {
logger := slog.CreateLogger().Level(level)
if prefix != "" {
logger.Prefix(prefix)
}
logger.AddWriter(logger.CreateJsonStdoutWriter())
return logger
}
// CreateFileLogger creates a logger with the shared default policy and appends
// file output to the provided path.
//
// The returned logger is always non-nil. When file writer creation fails, the
// logger still writes to stdout and the error is returned to the caller.
func CreateFileLogger(prefix string, level slog.LogLevel, filePath string) (*slog.Logger, error) {
logger := CreateLogger(prefix, level)
fileWriter, err := logger.CreateTextFileWriter(filePath)
if err != nil {
return logger, err
}
logger.AddWriter(fileWriter)
return logger, nil
}
+34
View File
@@ -0,0 +1,34 @@
package utils
import (
"os"
"path/filepath"
"strings"
"testing"
"git.nix13.pw/scuroneko/slog"
)
func TestCreateFileLoggerWritesToConfiguredFile(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "main.log")
logger, err := CreateFileLogger("TEST", slog.DEBUG, logPath)
if err != nil {
t.Fatalf("CreateFileLogger returned error: %v", err)
}
logger.Infoln("hello from file logger")
if err := logger.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile returned error: %v", err)
}
if !strings.Contains(string(data), "hello from file logger") {
t.Fatalf("expected log message in file, got %q", string(data))
}
if !strings.Contains(string(data), "[TEST]") {
t.Fatalf("expected prefix in file, got %q", string(data))
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
package utils
const (
VersionString = "1.0.0-rc.4"
VersionString = "1.0.0-rc.8"
VersionMajor = 1
VersionMinor = 0
VersionPatch = 0
VersionBeta = 4
VersionBeta = 8
)