Compare commits

..

23 Commits

Author SHA1 Message Date
a49827afc2 Update libs 2023-11-19 16:07:49 +03:00
9e122d1a12 updated echotron version 2023-06-16 01:07:14 +03:00
57d11c7671 Fixed markdown escaping 2023-06-15 17:04:09 +03:00
b04a779236 Types of callbacks 2023-06-13 02:23:09 +03:00
9298c6402b MessageTextOptions 2023-06-08 01:44:03 +03:00
01d04199fd Callbacks 2023-06-07 21:22:50 +03:00
f4346d8859 Updated Go version 2023-06-05 01:34:53 +03:00
dd0307e382 Constructors for keyboard and options, CallbackHandler 2023-04-13 11:14:37 +03:00
b7f1e3e9ed PhotoOptions and VideoOptions 2023-02-13 01:39:57 +03:00
c6ce1913dc Added OptionsBuilder 2023-02-10 16:59:42 +03:00
1377e03b5e Filters, ChainRun, UserMention 2023-01-24 23:16:58 +03:00
c0143707e9 Changed Param method of Command 2023-01-24 03:10:57 +03:00
d035524ebc Lists of commands 2023-01-24 03:08:34 +03:00
e396d6a885 Moved IsUserAdmin from Update, added Chat method 2023-01-24 02:25:08 +03:00
906c769453 Removed command handler 2023-01-24 01:12:19 +03:00
d797a52029 Moved logging away for handling extarnally 2023-01-24 01:08:24 +03:00
599584ea58 Middleware 2023-01-24 00:37:08 +03:00
b074189d31 Allow private chats use admin commands 2023-01-23 11:54:26 +03:00
e2ba9565d5 Dispatcher, Commands, Filters, Markdown escaping 2023-01-22 23:54:54 +03:00
677f28182a Custom API server 2023-01-20 16:32:13 +03:00
3a06ae5f0e Added state interface for customizing 2023-01-20 01:44:35 +03:00
1a8decf4d4 Changed State 2023-01-20 00:27:29 +03:00
5842815c95 Go module initiated 2023-01-19 23:17:47 +03:00
14 changed files with 592 additions and 41 deletions

31
bot.go
View File

@ -2,6 +2,7 @@ package nechotron
import ( import (
"context" "context"
"sync"
"time" "time"
echo "github.com/NicoNex/echotron/v3" echo "github.com/NicoNex/echotron/v3"
@ -9,10 +10,18 @@ import (
) )
type bot struct { type bot struct {
chatID int64
me *echo.User
echo.API echo.API
state *State chatID int64
Me *echo.User
data StateData
lock sync.Mutex
State Runnable
handler UpdateHandler
}
func DefaultHandler(u *Update) error {
err := u.Bot.State.Call(u)
return err
} }
func (b *bot) Update(u *echo.Update) { func (b *bot) Update(u *echo.Update) {
@ -25,11 +34,13 @@ func (b *bot) Update(u *echo.Update) {
Ctx: ctx, Ctx: ctx,
} }
newState, err := b.state.Fn(upd) b.lock.Lock()
if err != nil { defer b.lock.Unlock()
upd.LogError("", err, true) // defer func() {
} // if r := recover(); r != nil {
if newState != nil { // log.Printf("[%s] Recovered. Error: %s\n", upd.UpdateID.String(), r)
b.state = newState // }
} // }()
b.handler(upd)
} }

25
callbacks.go Normal file
View File

@ -0,0 +1,25 @@
package nechotron
import (
"context"
"strings"
)
type CallbackFilter func(u *Update) bool
func CallbackExact(text string) CallbackFilter {
return func(u *Update) bool {
data := u.Callback()
return text == data
}
}
func CallbackPrefix(text string) CallbackFilter {
return func(u *Update) bool {
data := u.Callback()
if strings.HasPrefix(data, text) {
u.Ctx = context.WithValue(u.Ctx, FilteredValue("cb_"+text), strings.TrimPrefix(data, text+":"))
}
return strings.HasPrefix(data, text)
}
}

61
command.go Normal file
View File

@ -0,0 +1,61 @@
package nechotron
import (
"fmt"
"log"
"strings"
"github.com/NicoNex/echotron/v3"
)
type Command struct {
Body string
IsAdminOnly bool
Description string
}
func NewCommand(body string, desc string, isAdminOnly bool) *Command {
return &Command{
Body: body,
IsAdminOnly: isAdminOnly,
Description: desc,
}
}
func (c *Command) String() string {
return "/" + c.Body
}
func (c *Command) Param(text string) string {
strs := strings.SplitN(text, " ", 2)
if len(strs) > 1 {
return strs[1]
}
return ""
}
func MakeCommandList(format string, commands ...*Command) string {
result := ""
for _, command := range commands {
result += fmt.Sprintf(format, command.String(), EscapeMd2(command.Description))
}
return result
}
func botCommands(commands ...*Command) []echotron.BotCommand {
cmds := []echotron.BotCommand{}
for _, c := range commands {
cmds = append(cmds, echotron.BotCommand{Command: c.Body, Description: c.Description})
}
return cmds
}
func SetMyCommands(api echotron.API, langCode string, scope echotron.BotCommandScope, commands ...*Command) error {
_, err := api.SetMyCommands(&echotron.CommandOptions{LanguageCode: "", Scope: scope}, botCommands(commands...)...)
if err != nil {
log.Println(err)
return err
}
return nil
}
// func HandleCommand(command *Command, handler cmdFunc) (bool, error)

104
dispatcher.go Normal file
View File

@ -0,0 +1,104 @@
package nechotron
import (
"strings"
"github.com/NicoNex/echotron/v3"
)
type UpdateHandler func(u *Update) error
type CommandHandler func(u *Update, arg string) error
type CallbackHandler func(u *Update, data string) error
type dispatchHandler func(u *Update) (bool, error)
type Dispatcher struct {
handlers []dispatchHandler
}
func NewDispatcher() *Dispatcher {
return &Dispatcher{}
}
func (d *Dispatcher) Run(u *Update) error {
for _, h := range d.handlers {
executed, err := h(u)
if executed {
return err
}
}
return nil
}
func (d *Dispatcher) Handle(handler func(u *Update) (bool, error)) *Dispatcher {
d.handlers = append(d.handlers, handler)
return d
}
func (d *Dispatcher) HandleCommand(command *Command, handler CommandHandler) *Dispatcher {
newHandler := func(u *Update) (bool, error) {
if !strings.HasPrefix(u.Text(), command.String()) {
return false, nil
}
if command.IsAdminOnly && !IsUserAdmin(u) && u.ChatID() < 0 {
return false, nil
}
err := handler(u, command.Param(u.Text()))
return true, err
}
d.handlers = append(d.handlers, newHandler)
return d
}
func (d *Dispatcher) HandleFilter(filter FilterFn, handler UpdateHandler) *Dispatcher {
newHandler := func(u *Update) (bool, error) {
if !filter(u) {
return false, nil
}
err := handler(u)
return true, err
}
d.handlers = append(d.handlers, newHandler)
return d
}
func (d *Dispatcher) HandleCallback(filter CallbackFilter, handler UpdateHandler) *Dispatcher {
newHandler := func(u *Update) (bool, error) {
if !u.IsCallback() {
return false, nil
}
defer u.Bot.AnswerCallbackQuery(u.CallbackQuery.ID, &echotron.CallbackQueryOptions{})
if !filter(u) {
return false, nil
}
err := handler(u)
return true, err
}
d.handlers = append(d.handlers, newHandler)
return d
}
func (d *Dispatcher) HandleReply(handler UpdateHandler) *Dispatcher {
newHandler := func(u *Update) (bool, error) {
if !u.IsMessage() {
return false, nil
}
if u.Message.ReplyToMessage == nil {
return false, nil
}
err := handler(u)
return true, err
}
d.handlers = append(d.handlers, newHandler)
return d
}
// Runs each dispatcher or handled listed here until error is returned from any
func RunEach(u *Update, disps ...*Dispatcher) error {
for _, d := range disps {
if err := d.Run(u); err != nil {
return err
}
}
return nil
}

71
filters.go Normal file
View File

@ -0,0 +1,71 @@
package nechotron
import (
"strings"
)
type FilterFn func(u *Update) bool
type FilteredValue interface{}
func TextStartsWith(text string) FilterFn {
return func(u *Update) bool {
return strings.HasPrefix(u.Text(), text)
}
}
func TextStartsWithAny(subs ...string) FilterFn {
return func(u *Update) bool {
text := u.Text()
for _, sub := range subs {
if strings.HasPrefix(text, sub) {
return true
}
}
return false
}
}
func TextHas(text string) FilterFn {
return func(u *Update) bool {
return strings.Contains(u.Text(), text)
}
}
func TextHasAny(subs ...string) FilterFn {
return func(u *Update) bool {
text := u.Text()
for _, sub := range subs {
if strings.Contains(text, sub) {
return true
}
}
return false
}
}
func IsPrivate(u *Update) bool {
return u.ChatID() > 0
}
func IsForum(u *Update) bool {
return u.Chat().IsForum
}
func IsReply(u *Update) bool {
return u.IsReply()
}
func IsCallback(u *Update) bool {
return u.IsCallback()
}
func IsUserAdmin(u *Update) bool {
if IsPrivate(u) {
return true
}
member, err := u.Bot.GetChatMember(u.ChatID(), u.From().ID)
if err != nil {
return false
}
return member.Result.Status == "administrator" || member.Result.Status == "creator"
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.nefrace.ru/nefrace/nechotron
go 1.21
require (
github.com/NicoNex/echotron/v3 v3.27.0
github.com/google/uuid v1.4.0
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/NicoNex/echotron/v3 v3.26.0 h1:kNO9AD0CNXAlbd96Z4jT8NHcn2OPD3wTXs6HnjXHXU0=
github.com/NicoNex/echotron/v3 v3.26.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw=
github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View File

@ -6,6 +6,10 @@ type InKeyboard struct {
Buttons [][]echotron.InlineKeyboardButton Buttons [][]echotron.InlineKeyboardButton
} }
func NewInlineKeyboard() *InKeyboard {
return &InKeyboard{}
}
func (i *InKeyboard) Row(buttons ...echotron.InlineKeyboardButton) *InKeyboard { func (i *InKeyboard) Row(buttons ...echotron.InlineKeyboardButton) *InKeyboard {
i.Buttons = append(i.Buttons, buttons) i.Buttons = append(i.Buttons, buttons)
return i return i

21
markdown.go Normal file
View File

@ -0,0 +1,21 @@
package nechotron
import (
"fmt"
"regexp"
"strings"
"github.com/NicoNex/echotron/v3"
)
var chars = []string{"_", "\\*", "\\[", "\\]", "\\(", "\\)", "~", "`", ">", "#", "\\+", "\\-", "=", "|", "{", "}", "\\.", "!"}
var r = strings.Join(chars, "")
var reg = regexp.MustCompile("[" + r + "]+")
func EscapeMd2(s string) string {
return reg.ReplaceAllString(s, "\\$0")
}
func UserMention(u *echotron.User) string {
return fmt.Sprintf("[%s](tg://user?id=%d)", EscapeMd2(u.FirstName), u.ID)
}

3
middleware.go Normal file
View File

@ -0,0 +1,3 @@
package nechotron
type Middleware func(next UpdateHandler) UpdateHandler

View File

@ -8,7 +8,9 @@ import (
type Nechotron struct { type Nechotron struct {
Token string Token string
DefaultState *State DefaultState Runnable
ApiServer string
Handler UpdateHandler
} }
func NewTron(token string, defaultState *State) *Nechotron { func NewTron(token string, defaultState *State) *Nechotron {
@ -19,20 +21,24 @@ func NewTron(token string, defaultState *State) *Nechotron {
return &Nechotron{ return &Nechotron{
Token: token, Token: token,
DefaultState: state, DefaultState: state,
Handler: DefaultHandler,
} }
} }
func (n *Nechotron) newBot(chatID int64) echo.Bot { func (n *Nechotron) newBot(chatID int64) echo.Bot {
a := echo.NewAPI(n.Token) a := echo.NewAPI(n.Token)
// a := echo.NewAPI(n.Token)
me, _ := a.GetMe() me, _ := a.GetMe()
// log.Println("New bot active: ", chatID, me.Result) // log.Println("New bot active: ", chatID, me.Result)
b := &bot{ b := &bot{
chatID: chatID, chatID: chatID,
me: me.Result, Me: me.Result,
API: a, API: a,
state: n.DefaultState, State: n.DefaultState,
data: make(StateData),
handler: n.Handler,
} }
b.state = n.DefaultState b.State = n.DefaultState
return b return b
} }
@ -41,3 +47,8 @@ func (n *Nechotron) DispatchPoll() error {
log.Println("Nechotron poll dispatcher started") log.Println("Nechotron poll dispatcher started")
return dispatcher.Poll() return dispatcher.Poll()
} }
func (n *Nechotron) Use(mid Middleware) *Nechotron {
n.Handler = mid(n.Handler)
return n
}

130
options.go Normal file
View File

@ -0,0 +1,130 @@
package nechotron
import "github.com/NicoNex/echotron/v3"
type OptionsBuilder struct {
caption string
replyMarkup echotron.ReplyMarkup
parseMode echotron.ParseMode
entities []echotron.MessageEntity
messageThreadID int64
replyToMessageID int
disableWebPagePreview bool
disableNotification bool
protectContent bool
allowSendingWithoutReply bool
hasSpoiler bool
}
func NewOptions() *OptionsBuilder {
return &OptionsBuilder{}
}
func (o *OptionsBuilder) Caption(text string) *OptionsBuilder {
o.caption = text
return o
}
func (o *OptionsBuilder) ReplyMarkup(markup echotron.ReplyMarkup) *OptionsBuilder {
o.replyMarkup = markup
return o
}
func (o *OptionsBuilder) MarkdownV2() *OptionsBuilder {
o.parseMode = echotron.MarkdownV2
return o
}
func (o *OptionsBuilder) Spoiler() *OptionsBuilder {
o.hasSpoiler = true
return o
}
func (o *OptionsBuilder) Entities(entities []echotron.MessageEntity) *OptionsBuilder {
o.entities = entities
return o
}
func (o *OptionsBuilder) Thread(id int64) *OptionsBuilder {
o.messageThreadID = id
return o
}
func (o *OptionsBuilder) ReplyTo(id int) *OptionsBuilder {
o.replyToMessageID = id
return o
}
func (o *OptionsBuilder) DisableNotification() *OptionsBuilder {
o.disableNotification = true
return o
}
func (o *OptionsBuilder) ProtectContent() *OptionsBuilder {
o.protectContent = true
return o
}
func (o *OptionsBuilder) DisableWebPagePreview() *OptionsBuilder {
o.disableWebPagePreview = true
return o
}
func (o *OptionsBuilder) AllowSendingWithoutReply() *OptionsBuilder {
o.allowSendingWithoutReply = true
return o
}
func (o *OptionsBuilder) MessageOptions() *echotron.MessageOptions {
return &echotron.MessageOptions{
ReplyMarkup: o.replyMarkup,
ParseMode: o.parseMode,
Entities: o.entities,
MessageThreadID: o.messageThreadID,
ReplyToMessageID: o.replyToMessageID,
DisableWebPagePreview: o.allowSendingWithoutReply,
DisableNotification: o.disableNotification,
ProtectContent: o.protectContent,
AllowSendingWithoutReply: o.allowSendingWithoutReply,
}
}
func (o *OptionsBuilder) MessageTextOptions() *echotron.MessageTextOptions {
return &echotron.MessageTextOptions{
ReplyMarkup: o.replyMarkup.(echotron.InlineKeyboardMarkup),
ParseMode: o.parseMode,
Entities: o.entities,
DisableWebPagePreview: o.allowSendingWithoutReply,
}
}
func (o *OptionsBuilder) PhotoOptions() *echotron.PhotoOptions {
return &echotron.PhotoOptions{
ReplyMarkup: o.replyMarkup,
ParseMode: o.parseMode,
Caption: o.caption,
CaptionEntities: o.entities,
MessageThreadID: int(o.messageThreadID),
ReplyToMessageID: o.replyToMessageID,
HasSpoiler: o.hasSpoiler,
DisableNotification: o.disableNotification,
ProtectContent: o.protectContent,
AllowSendingWithoutReply: o.allowSendingWithoutReply,
}
}
func (o *OptionsBuilder) VideoOptions() *echotron.VideoOptions {
return &echotron.VideoOptions{
ReplyMarkup: o.replyMarkup,
ParseMode: o.parseMode,
Caption: o.caption,
CaptionEntities: o.entities,
SupportsStreaming: true,
MessageThreadID: int(o.messageThreadID),
ReplyToMessageID: o.replyToMessageID,
HasSpoiler: o.hasSpoiler,
DisableNotification: o.disableNotification,
ProtectContent: o.protectContent,
AllowSendingWithoutReply: o.allowSendingWithoutReply,
}
}

View File

@ -1,23 +1,55 @@
package nechotron package nechotron
import ( import (
"context" "errors"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
) )
type State struct { type StateData map[string]interface{}
Fn stateFn
Data context.Context type Runnable interface {
Call(*Update) error
} }
type stateFn func(*Update) (*State, error) type State struct {
Fn stateFn
}
func (s *State) Call(u *Update) error {
return s.Fn(u)
}
type stateFn func(*Update) error
func (d StateData) Set(name string, data interface{}) {
d[name] = data
}
func (d StateData) Get(name string) (interface{}, error) {
data, ok := d[name]
if !ok {
return nil, errors.New("can't get data from statedata")
}
return data, nil
}
func (d StateData) Has(name string) bool {
_, ok := d[name]
return ok
}
func (d StateData) Clear() {
for k := range d {
delete(d, k)
}
}
var EchoState = State{ var EchoState = State{
Fn: EchoFunc, Fn: EchoFunc,
} }
func EchoFunc(u *Update) (*State, error) { func EchoFunc(u *Update) error {
u.AnswerText(u.Text(), &echotron.MessageOptions{}) u.AnswerText(u.Text(), &echotron.MessageOptions{})
return nil, nil return nil
} }

View File

@ -13,6 +13,10 @@ type U echotron.Update
var emptyOpts = echotron.MessageOptions{} var emptyOpts = echotron.MessageOptions{}
type Event interface {
Upd() *echotron.Update
}
type Update struct { type Update struct {
U U
Bot *bot Bot *bot
@ -20,10 +24,35 @@ type Update struct {
Ctx context.Context Ctx context.Context
} }
type UpdateCommand struct {
Update
Param string
}
func (u *Update) Upd() *echotron.Update { func (u *Update) Upd() *echotron.Update {
return (*echotron.Update)(&u.U) return (*echotron.Update)(&u.U)
} }
func (u *Update) Chat() echotron.Chat {
if u.IsMessage() {
return u.Message.Chat
}
if u.IsCallback() {
return u.CallbackQuery.Message.Chat
}
if u.EditedMessage != nil {
return u.EditedMessage.Chat
}
if u.ChatMember != nil {
return u.ChatMember.Chat
}
if u.MyChatMember != nil {
return u.MyChatMember.Chat
}
log.Fatalf("[%s] Can't get ChatID of update %v+", u.UpdateID, u.U)
return echotron.Chat{}
}
func (u *Update) ChatID() int64 { func (u *Update) ChatID() int64 {
if u.IsMessage() { if u.IsMessage() {
return u.Message.Chat.ID return u.Message.Chat.ID
@ -44,6 +73,18 @@ func (u *Update) MessageID() int {
log.Fatalf("[%s] Can't get ChatID of update %v+", u.UpdateID, u.U) log.Fatalf("[%s] Can't get ChatID of update %v+", u.UpdateID, u.U)
return 0 return 0
} }
func (u *Update) ThreadID() int64 {
if !u.Chat().IsForum {
return 0
}
if u.IsMessage() {
return int64(u.Message.ThreadID)
}
if u.IsCallback() {
return int64(u.CallbackQuery.Message.ThreadID)
}
return 0
}
func (u *Update) AnswerText(text string, options *echotron.MessageOptions) (echotron.APIResponseMessage, error) { func (u *Update) AnswerText(text string, options *echotron.MessageOptions) (echotron.APIResponseMessage, error) {
return u.Bot.SendMessage(text, u.ChatID(), options) return u.Bot.SendMessage(text, u.ChatID(), options)
@ -54,12 +95,15 @@ func (u *Update) EditText(text string, options *echotron.MessageTextOptions) (ec
} }
func (u *Update) AnswerPlain(text string) (echotron.APIResponseMessage, error) { func (u *Update) AnswerPlain(text string) (echotron.APIResponseMessage, error) {
return u.Bot.SendMessage(text, u.ChatID(), &emptyOpts) return u.Bot.SendMessage(text, u.ChatID(), &echotron.MessageOptions{
MessageThreadID: u.ThreadID(),
})
} }
func (u *Update) AnswerMarkdown(text string) (echotron.APIResponseMessage, error) { func (u *Update) AnswerMarkdown(text string) (echotron.APIResponseMessage, error) {
return u.Bot.SendMessage(text, u.ChatID(), &echotron.MessageOptions{ return u.Bot.SendMessage(text, u.ChatID(), &echotron.MessageOptions{
ParseMode: echotron.MarkdownV2, ParseMode: echotron.MarkdownV2,
MessageThreadID: u.ThreadID(),
}) })
} }
@ -87,6 +131,9 @@ func (u *Update) IsMessage() bool {
func (u *Update) IsCallback() bool { func (u *Update) IsCallback() bool {
return u.CallbackQuery != nil return u.CallbackQuery != nil
} }
func (u *Update) IsReply() bool {
return u.IsMessage() && u.Message.ReplyToMessage != nil
}
func (u *Update) IsButton(b *Button) bool { func (u *Update) IsButton(b *Button) bool {
if u.IsText() && u.Text() == b.text { if u.IsText() && u.Text() == b.text {
return true return true
@ -151,14 +198,43 @@ func (u *Update) From() *echotron.User {
if u.IsMessage() { if u.IsMessage() {
return u.Message.From return u.Message.From
} }
if u.EditedMessage != nil {
return u.EditedMessage.From
}
if u.ChatMember != nil {
return &u.ChatMember.From
}
if u.MyChatMember != nil {
return &u.MyChatMember.From
}
return u.CallbackQuery.From return u.CallbackQuery.From
} }
func GetText(u *echotron.Message) string {
if u.Text != "" {
return u.Text
}
if u.Caption != "" {
return u.Caption
}
return ""
}
func (u *Update) Text() string { func (u *Update) Text() string {
if u.IsText() { if u.IsText() {
return u.Message.Text return u.Message.Text
} }
return u.Message.Caption if u.IsMessage() {
return u.Message.Caption
}
return ""
}
func (u *Update) Callback() string {
if u.IsCallback() {
return u.CallbackQuery.Data
}
return ""
} }
func (u *Update) Entities() []*echotron.MessageEntity { func (u *Update) Entities() []*echotron.MessageEntity {
@ -168,27 +244,13 @@ func (u *Update) Entities() []*echotron.MessageEntity {
return u.Message.CaptionEntities return u.Message.CaptionEntities
} }
func (u *Update) IsUserAdmin() bool {
ids := []int64{
60441930, // v.rud
327487258, // vika shet
}
from := u.From()
for i := range ids {
if from.ID == ids[i] {
return true
}
}
return false
}
func (u *Update) LogError(text string, e error, send bool) { func (u *Update) LogError(text string, e error, send bool) {
if text != "" { if text != "" {
text = "Ошибка: " + text text = "Ошибка: " + text
} else { } else {
text = "Произошла внутренняя ошибка" text = "Произошла внутренняя ошибка"
} }
log.Printf("[%s] %s: %v", u.UpdateID.String(), text, e) log.Printf("ERROR :: [%s] :: %s: %v", u.UpdateID.String(), text, e)
if send { if send {
u.AnswerMarkdown(fmt.Sprintf("%s\nКод запроса: `%s`", text, u.UpdateID.String())) u.AnswerMarkdown(fmt.Sprintf("%s\nКод запроса: `%s`", text, u.UpdateID.String()))
} }