save the work about getting rid of pointers.

This commit is contained in:
Andrey Parhomenko 2024-03-28 12:41:09 +05:00
parent 5186fec028
commit 3f2f16a79e
14 changed files with 348 additions and 413 deletions

View file

@ -1,13 +0,0 @@
package tg
import (
//"reflect"
)
// The argument for handling in channenl behaviours.
type ChannelContext struct {
}
type CC = ChannelContext
type ChannelAction struct {
Act (*ChannelContext)
}

65
bot.go
View file

@ -13,16 +13,15 @@ type User = tgbotapi.User
// The wrapper around Telegram API. // The wrapper around Telegram API.
type Bot struct { type Bot struct {
// Custom data value. // Custom data value.
Data any data any
Api *tgbotapi.BotAPI api *tgbotapi.BotAPI
Me *User me User
// Private bot behaviour. // Private bot behaviour.
behaviour *Behaviour behaviour *Behaviour
// Group bot behaviour. // Group bot behaviour.
//groupBehaviour *GroupBehaviour //groupBehaviour *GroupBehaviour
// Bot behaviour in channels. // Bot behaviour in channels.
//channelBehaviour *ChannelBehaviour //channelBehaviour *ChannelBehaviour
contexts map[SessionId] *context
sessions SessionMap sessions SessionMap
//groupSessions GroupSessionMap //groupSessions GroupSessionMap
} }
@ -35,40 +34,49 @@ func NewBot(token string) (*Bot, error) {
} }
return &Bot{ return &Bot{
Api: bot, api: bot,
contexts: make(map[SessionId] *context),
}, nil }, nil
} }
func (bot *Bot) Debug(debug bool) *Bot { func (bot *Bot) Debug(debug bool) *Bot {
bot.Api.Debug = debug bot.api.Debug = debug
return bot return bot
} }
func (bot *Bot) Api() *tgbotapi.BotAPI {
return bot.api
}
func (bot *Bot) Me() User {
return bot.me
}
// Send the Renderable to the specified session client side. // Send the Renderable to the specified session client side.
// Can be used for both group and private sessions because // Can be used for both group and private sessions because
// SessionId represents both for chat IDs. // SessionId represents both for chat IDs.
func (bot *Bot) Send( func (bot *Bot) Send(
sid SessionId, v Sendable, sid SessionId, v Sendable,
) (*Message, error) { ) (Message, error) {
config := v.SendConfig(sid, bot) config := v.SendConfig(sid, bot)
if config.Error != nil { if config.Error != nil {
return nil, config.Error return Message{}, config.Error
} }
msg, err := bot.Api.Send(config.ToApi()) msg, err := bot.api.Send(config.ToApi())
if err != nil { if err != nil {
return nil, err return Message{}, err
} }
return &msg, nil v.SetMessage(msg)
return msg, nil
} }
func (bot *Bot) Sendf( func (bot *Bot) Sendf(
sid SessionId, format string, v ...any, sid SessionId, format string, v ...any,
) (*Message, error){ ) (Message, error){
msg := Messagef(format, v...)
return bot.Send( return bot.Send(
sid, sid,
NewMessage(format, v...), &msg,
) )
} }
@ -76,7 +84,7 @@ func (bot *Bot) Sendf(
func (bot *Bot) SendRaw( func (bot *Bot) SendRaw(
sid SessionId, v tgbotapi.Chattable, sid SessionId, v tgbotapi.Chattable,
) (*Message, error) { ) (*Message, error) {
msg, err := bot.Api.Send(v) msg, err := bot.api.Send(v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -117,7 +125,7 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot {
func (bot *Bot) DeleteCommands() { func (bot *Bot) DeleteCommands() {
//tgbotapi.NewBotCommandScopeAllPrivateChats(), //tgbotapi.NewBotCommandScopeAllPrivateChats(),
cfg := tgbotapi.NewDeleteMyCommands() cfg := tgbotapi.NewDeleteMyCommands()
bot.Api.Request(cfg) bot.api.Request(cfg)
} }
// Setting the command on the user side. // Setting the command on the user side.
@ -151,7 +159,7 @@ func (bot *Bot) SetCommands(
botCmds..., botCmds...,
) )
_, err := bot.Api.Request(cfg) _, err := bot.api.Request(cfg)
if err != nil { if err != nil {
return err return err
} }
@ -170,11 +178,11 @@ func (bot *Bot) Run() error {
uc := tgbotapi.NewUpdate(0) uc := tgbotapi.NewUpdate(0)
uc.Timeout = 10 uc.Timeout = 10
updates := bot.Api.GetUpdatesChan(uc) updates := bot.api.GetUpdatesChan(uc)
handles := make(map[string] chan *Update) handles := make(map[string] chan Update)
if bot.behaviour != nil { if bot.behaviour != nil {
chn := make(chan *Update) chn := make(chan Update)
handles["private"] = chn handles["private"] = chn
go bot.handlePrivate(chn) go bot.handlePrivate(chn)
} }
@ -195,10 +203,10 @@ func (bot *Bot) Run() error {
}*/ }*/
me, _ := bot.Api.GetMe() me, _ := bot.Api.GetMe()
bot.Me = &me bot.me = me
for up := range updates { for up := range updates {
u := &Update{ u := Update{
Update: &up, Update: up,
} }
// Sometimes returns nil. // Sometimes returns nil.
@ -220,7 +228,7 @@ func (bot *Bot) Run() error {
// The function handles updates supposed for the private // The function handles updates supposed for the private
// chat with the bot. // chat with the bot.
func (bot *Bot) handlePrivate(updates chan *Update) { func (bot *Bot) handlePrivate(updates chan Update) {
var sid SessionId var sid SessionId
for u := range updates { for u := range updates {
sid = SessionId(u.FromChat().ID) sid = SessionId(u.FromChat().ID)
@ -245,12 +253,13 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
bot.contexts[sid] = ctx bot.contexts[sid] = ctx
} }
go (&Context{ go Context{
context: ctx, session: session,
bot: bot,
Update: u, Update: u,
input: ctx.updates, input: ctx.updates,
}).serve() }.serve()
ctx.updates.Send(u) ctx.session.updates.Send(u)
continue continue
} }

View file

@ -16,7 +16,7 @@ type Button struct {
Action Action Action Action
} }
type ButtonMap map[string]*Button type ButtonMap map[string]Button
// Returns the only location button in the map. // Returns the only location button in the map.
func (btnMap ButtonMap) LocationButton() *Button { func (btnMap ButtonMap) LocationButton() *Button {
@ -32,14 +32,14 @@ func (btnMap ButtonMap) LocationButton() *Button {
type ButtonRow []*Button type ButtonRow []*Button
// Returns new button with the specified text and no action. // Returns new button with the specified text and no action.
func NewButton(format string, v ...any) *Button { func Buttonf(format string, v ...any) Button {
return &Button{ return &Button{
Text: fmt.Sprintf(format, v...), Text: fmt.Sprintf(format, v...),
} }
} }
// Randomize buttons data to make the key unique. // Randomize buttons data to make the key unique.
func (btn *Button) Rand() *Button { func (btn Button) Rand() Button {
rData := make([]byte, 8) rData := make([]byte, 8)
rand.Read(rData) rand.Read(rData)
data := make([]byte, base64.StdEncoding.EncodedLen(len(rData))) data := make([]byte, base64.StdEncoding.EncodedLen(len(rData)))
@ -49,41 +49,37 @@ func (btn *Button) Rand() *Button {
} }
// Set the URL for the button. Only for inline buttons. // Set the URL for the button. Only for inline buttons.
func (btn *Button) WithUrl(format string, v ...any) *Button { func (btn Button) WithUrl(format string, v ...any) Button {
btn.Url = fmt.Sprintf(format, v...) btn.Url = fmt.Sprintf(format, v...)
return btn return btn
} }
// Set the action when pressing the button. // Set the action when pressing the button.
// By default is nil and does nothing. // By default is nil and does nothing.
func (btn *Button) WithAction(a Action) *Button { func (btn Button) WithAction(a Action) Button {
btn.Action = a btn.Action = a
return btn return btn
} }
func (btn *Button) WithData(dat string) *Button { func (btn Button) WithData(dat string) Button {
btn.Data = dat btn.Data = dat
return btn return btn
} }
// Sets whether the button must send owner's location. // Sets whether the button must send owner's location.
func (btn *Button) WithSendLocation(ok bool) *Button { func (btn Button) WithSendLocation(ok bool) Button {
btn.SendLocation = ok btn.SendLocation = ok
return btn return btn
} }
func (btn *Button) ActionFunc(fn ActionFunc) *Button { func (btn Button) Go(pth Path, args ...any) Button {
return btn.WithAction(fn)
}
func (btn *Button) Go(pth Path, args ...any) *Button {
return btn.WithAction(ScreenGo{ return btn.WithAction(ScreenGo{
Path: pth, Path: pth,
Args: args, Args: args,
}) })
} }
func (btn *Button) ToTelegram() apix.KeyboardButton { func (btn Button) ToTelegram() apix.KeyboardButton {
ret := apix.NewKeyboardButton(btn.Text) ret := apix.NewKeyboardButton(btn.Text)
if btn.SendLocation { if btn.SendLocation {
ret.RequestLocation = true ret.RequestLocation = true
@ -91,7 +87,7 @@ func (btn *Button) ToTelegram() apix.KeyboardButton {
return ret return ret
} }
func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton { func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
if btn.Data != "" { if btn.Data != "" {
return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data) return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data)
} }
@ -105,7 +101,7 @@ func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton {
} }
// Return the key of the button to identify it by messages and callbacks. // Return the key of the button to identify it by messages and callbacks.
func (btn *Button) Key() string { func (btn Button) Key() string {
if btn == nil { if btn == nil {
return "" return ""
} }

View file

@ -1,8 +1,6 @@
package tg package tg
import ( import (
//"flag"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
@ -14,17 +12,17 @@ const (
) )
type CommandName string type CommandName string
type Command struct { type Command struct {
Name CommandName Name CommandName
Type CommandType Type CommandType
Description string Description string
Action Action Action Action
Widget Widget Widget Widget
} }
type CommandMap map[CommandName]*Command type CommandMap map[CommandName]*Command
func NewCommand(name CommandName, desc string) *Command { func NewCommand(name CommandName, desc string) Command {
if name == "" || desc == "" { if name == "" || desc == "" {
panic("name and description cannot be an empty string") panic("name and description cannot be an empty string")
} }
@ -39,27 +37,23 @@ func (c *Command) WithAction(a Action) *Command {
return c return c
} }
func (c *Command) ActionFunc(af ActionFunc) *Command { func (c Command) WithWidget(w Widget) Command {
return c.WithAction(af)
}
func (c *Command) WithWidget(w Widget) *Command {
c.Widget = w c.Widget = w
return c return c
} }
func (c *Command) WidgetFunc(fn Func) *Command { func (c Command) WidgetFunc(fn Func) Command {
return c.WithWidget(fn) return c.WithWidget(fn)
} }
func (c *Command) ToApi() tgbotapi.BotCommand { func (c Command) ToApi() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{} ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name) ret.Command = string(c.Name)
ret.Description = c.Description ret.Description = c.Description
return ret return ret
} }
func (c *Command) Go(pth Path, args ...any) *Command { func (c Command) Go(pth Path, args ...any) Command {
return c.WithAction(ScreenGo{ return c.WithAction(ScreenGo{
Path: pth, Path: pth,
Args: args, Args: args,
@ -76,13 +70,12 @@ type CommandCompo struct {
// Returns new empty CommandCompo. // Returns new empty CommandCompo.
func NewCommandCompo(cmds ...*Command) *CommandCompo { func NewCommandCompo(cmds ...*Command) *CommandCompo {
ret := (&CommandCompo{}).WithCommands(cmds...) ret := CommandCompo{}.WithCommands(cmds...)
//ret.Commands = make(CommandMap)
return ret return ret
} }
// Set the commands to handle. // Set the commands to handle.
func (w *CommandCompo) WithCommands(cmds ...*Command) *CommandCompo { func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo {
if w.Commands == nil { if w.Commands == nil {
w.Commands = make(CommandMap) w.Commands = make(CommandMap)
} }
@ -105,23 +98,18 @@ func (w *CommandCompo) WithPreStart(a Action) *CommandCompo {
return w return w
} }
// Set the prestart action with function.
func (w *CommandCompo) WithPreStartFunc(fn ActionFunc) *CommandCompo {
return w.WithPreStart(fn)
}
// Set the usage action. // Set the usage action.
func (w *CommandCompo) WithUsage(a Action) *CommandCompo { func (w CommandCompo) WithUsage(a Action) *CommandCompo {
w.Usage = a w.Usage = a
return w return w
} }
// Set the usage action with function. // Set the usage action with function.
func (w *CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo { func (w CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo {
return w.WithUsage(fn) return w.WithUsage(fn)
} }
func (widget *CommandCompo) Filter( func (widget CommandCompo) Filter(
u *Update, u *Update,
) bool { ) bool {
if u.Message == nil || !u.Message.IsCommand() { if u.Message == nil || !u.Message.IsCommand() {
@ -132,13 +120,13 @@ func (widget *CommandCompo) Filter(
} }
// Implementing server. // Implementing server.
func (compo *CommandCompo) Serve(c *Context) { func (compo CommandCompo) Serve(c Context) {
/*commanders := make(map[CommandName] BotCommander) /*commanders := make(map[CommandName] BotCommander)
for k, v := range compo.Commands { for k, v := range compo.Commands {
commanders[k] = v commanders[k] = v
}*/ }*/
c.Bot.DeleteCommands() c.bot.DeleteCommands()
err := c.Bot.SetCommands( err := c.bot.SetCommands(
tgbotapi.NewBotCommandScopeChat(c.Session.Id.ToApi()), tgbotapi.NewBotCommandScopeChat(c.Session.Id.ToApi()),
compo.Commands, compo.Commands,
) )

View file

@ -8,57 +8,17 @@ import (
//"path" //"path"
) )
func Go(pth Path) UI {
return UI{
GoWidget(pth),
}
}
type GoWidget string
// Implementing the Server interface.
func (widget GoWidget) Serve(c *Context) {
c.input.Close()
c.Go(Path(widget))
}
func (widget GoWidget) Render(c *Context) UI {
return UI{widget}
}
func (widget GoWidget) Filter(u *Update) bool {
return true
}
// General context for a specific user.
// Is always the same and is not reached
// inside end function-handlers.
type context struct {
Session *Session
// To reach the bot abilities inside callbacks.
Bot *Bot
// Costum status for currently running context.
Status any
Type ContextType
updates *UpdateChan
skippedUpdates *UpdateChan
// Current screen ID.
pathHistory []Path
//path, prevPath Path
}
type Contexter interface {
GetContext() *Context
}
// Interface to interact with the user. // Interface to interact with the user.
type Context struct { type Context struct {
*context session *Session
// The update that called the Context usage. // The update that called the Context usage.
*Update update Update
// Used as way to provide outer values redirection // Used as way to provide outer values redirection
// into widgets and actions. It is like arguments // into widgets and actions. It is like arguments
// for REST API request etc. // for REST API request etc.
arg any arg any
typ ContextType
// Instead of updates as argument. // Instead of updates as argument.
input *UpdateChan input *UpdateChan
} }
@ -68,7 +28,7 @@ type Context struct {
// maybe you will find another usage for this. // maybe you will find another usage for this.
// Returns users context by specified session ID // Returns users context by specified session ID
// or nil if the user is not logged in. // or nil if the user is not logged in.
func (c *Context) As(sid SessionId) *Context { func (c Context) As(sid SessionId) Context {
n, ok := c.Bot.contexts[sid] n, ok := c.Bot.contexts[sid]
if !ok { if !ok {
return nil return nil
@ -78,17 +38,13 @@ func (c *Context) As(sid SessionId) *Context {
} }
} }
func (c *Context) GetContext() *Context {
return c
}
// General type function to define actions, single component widgets // General type function to define actions, single component widgets
// and components themselves. // and components themselves.
type Func func(*Context) type Func func(Context)
func (f Func) Act(c *Context) { func (f Func) Act(c Context) {
f(c) f(c)
} }
func (f Func) Serve(c *Context) { func (f Func) Serve(c Context) {
f(c) f(c)
} }
func(f Func) Filter(_ *Update) bool { func(f Func) Filter(_ *Update) bool {
@ -108,13 +64,13 @@ const (
) )
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *Context) serve() { func (c Context) serve() {
beh := c.Bot.behaviour beh := c.Bot.behaviour
c.Run(beh.Init) c.Run(beh.Init)
beh.Root.Serve(c) beh.Root.Serve(c)
} }
func (c *Context) Path() Path { func (c Context) Path() Path {
ln := len(c.pathHistory) ln := len(c.pathHistory)
if ln == 0 { if ln == 0 {
return "" return ""
@ -122,11 +78,11 @@ func (c *Context) Path() Path {
return c.pathHistory[ln-1] return c.pathHistory[ln-1]
} }
func (c *Context) Arg() any { func (c Context) Arg() any {
return c.arg return c.arg
} }
func (c *Context) Run(a Action) { func (c Context) Run(a Action) {
if a != nil { if a != nil {
a.Act(c) a.Act(c)
} }
@ -135,12 +91,12 @@ func (c *Context) Run(a Action) {
// Only for the root widget usage. // Only for the root widget usage.
// Skip the update sending it down to // Skip the update sending it down to
// the underlying widget. // the underlying widget.
func (c *Context) Skip(u *Update) { func (c Context) Skip(u Update) {
c.skippedUpdates.Send(u) c.skippedUpdates.Send(u)
} }
// Sends to the Sendable object. // Sends to the Sendable object.
func (c *Context) Send(v Sendable) (*Message, error) { func (c Context) Send(v Sendable) (Message, error) {
config := v.SendConfig(c.Session.Id, c.Bot) config := v.SendConfig(c.Session.Id, c.Bot)
if config.Error != nil { if config.Error != nil {
return nil, config.Error return nil, config.Error
@ -155,219 +111,79 @@ func (c *Context) Send(v Sendable) (*Message, error) {
// Sends the formatted with fmt.Sprintf message to the user // Sends the formatted with fmt.Sprintf message to the user
// using default Markdown parsing format. // using default Markdown parsing format.
func (c *Context) Sendf(format string, v ...any) (*Message, error) { func (c Context) Sendf(format string, v ...any) (Message, error) {
return c.Send(NewMessage(format, v...)) return c.Send(NewMessage(format, v...))
} }
// Same as Sendf but uses Markdown 2 format for parsing. // Same as Sendf but uses Markdown 2 format for parsing.
func (c *Context) Sendf2(format string, v ...any) (*Message, error) { func (c Context) Sendf2(format string, v ...any) (Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2()) return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2())
} }
// Same as Sendf but uses HTML format for parsing. // Same as Sendf but uses HTML format for parsing.
func (c *Context) SendfHTML(format string, v ...any) (*Message, error) { func (c Context) SendfHTML(format string, v ...any) (Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML()) return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML())
} }
func (c *Context) SendfR(format string, v ...any) (*Message, error) { // Send the message in raw format escaping all the special characters.
func (c Context) SendfR(format string, v ...any) (Message, error) {
return c.Send(NewMessage(Escape2(fmt.Sprintf(format, v...))).MD2()) return c.Send(NewMessage(Escape2(fmt.Sprintf(format, v...))).MD2())
} }
// Get the input for current widget. // Get the input for current widget.
// Should be used inside handlers (aka "Serve"). // Should be used inside handlers (aka "Serve").
func (c *Context) Input() chan *Update { func (c Context) Input() chan Update {
return c.input.Chan() return c.input.Chan()
} }
// Returns copy of current context so func (c Context) WithArg(v any) Context {
// it will not affect the current one.
// But be careful because
// most of the insides uses pointers
// which are not deeply copied.
func (c *Context) Copy() *Context {
ret := *c
return &ret
}
func (c *Context) WithArg(v any) *Context {
c = c.Copy()
c.arg = v c.arg = v
return c return c
} }
func (c *Context) WithUpdate(u *Update) *Context { func (c Context) WithUpdate(u *Update) Context {
c = c.Copy()
c.Update = u c.Update = u
return c return c
} }
func (c *Context) WithInput(input *UpdateChan) *Context { func (c Context) WithInput(input *UpdateChan) Context {
c = c.Copy()
c.input = input c.input = input
return c return c
} }
func (c Context) Go(pth Path) error {
return c.session.go_(pth, nil)
}
func (c Context) GoWithArg(pth Path, arg any) error {
return c.session.go_(pth, arg)
}
// Customized actions for the bot. // Customized actions for the bot.
type Action interface { type Action interface {
Act(*Context) Act(Context)
} }
type ActionFunc func(*Context) type ActionFunc func(Context)
func (af ActionFunc) Act(c *Context) { func (af ActionFunc) Act(c *Context) {
af(c) af(c)
} }
func (c *Context) History() []Path { func (c Context) History() []Path {
return c.pathHistory return c.session.pathHistory
} }
// Changes screen of user to the Id one. func (c Context) PathExist(pth Path) bool {
func (c *Context) Go(pth Path, args ...any) error { return c.bot.behaviour.PathExist(pth)
var err error
if pth == "" {
c.pathHistory = []Path{}
return nil
}
var back bool
if pth == "-" {
if len(c.pathHistory) < 2 {
return c.Go("")
}
pth = c.pathHistory[len(c.pathHistory)-2]
c.pathHistory = c.pathHistory[:len(c.pathHistory)-1]
}
// Getting the screen and changing to
// then executing its widget.
if !pth.IsAbs() {
pth = (c.Path() + "/" + pth).Clean()
}
if !c.PathExist(pth) {
return ScreenNotExistErr
}
if !back && c.Path() != pth {
c.pathHistory = append(c.pathHistory, pth)
}
// Stopping the current widget.
screen := c.Bot.behaviour.Screens[pth]
c.skippedUpdates.Close()
if screen.Widget != nil {
c.skippedUpdates, err = c.RunWidget(screen.Widget, args...)
if err != nil {
return err
}
} else {
return NoWidgetForScreenErr
}
return nil
} }
func (c *Context) PathExist(pth Path) bool { // Simple way to read strings for widgets with
return c.Bot.behaviour.PathExist(pth) // the specified prompt.
} func (c Context) ReadString(promptf string, args ...any) string {
func (c *Context) makeArg(args []any) any {
var arg any
if len(args) == 1 {
arg = args[0]
} else if len(args) > 1 {
arg = args
}
return arg
}
func (c *Context) RunCompo(compo Component, args ...any) (*UpdateChan, error) {
if compo == nil {
return nil, nil
}
s, ok := compo.(Sendable)
if ok {
msg, err := c.Send(s)
if err != nil {
return nil, err
}
s.SetMessage(msg)
}
updates := NewUpdateChan()
go func() {
compo.Serve(
c.WithInput(updates).
WithArg(c.makeArg(args)),
)
// To let widgets finish themselves before
// the channel is closed and close it by themselves.
updates.Close()
}()
return updates, nil
}
// Run widget in background returning the new input channel for it.
func (c *Context) RunWidget(widget Widget, args ...any) (*UpdateChan, error) {
var err error
if widget == nil {
return nil, EmptyWidgetErr
}
pth := c.Path()
compos := widget.Render(c.WithArg(c.makeArg(args)))
// Leave if changed path or components are empty.
if compos == nil || pth != c.Path() {
return nil, EmptyCompoErr
}
chns := make([]*UpdateChan, len(compos))
for i, compo := range compos {
chns[i], err = c.RunCompo(compo, args...)
if err != nil {
for _, chn := range chns {
chn.Close()
}
return nil, err
}
}
ret := NewUpdateChan()
go func() {
ln := len(compos)
UPDATE:
for u := range ret.Chan() {
if u == nil {
break
}
cnt := 0
for i, compo := range compos {
chn := chns[i]
if chn.Closed() {
cnt++
continue
}
if !compo.Filter(u) {
chn.Send(u)
continue UPDATE
}
}
if cnt == ln {
break
}
}
ret.Close()
for _, chn := range chns {
chn.Close()
}
}()
return ret, nil
}
// Simple way to read strings for widgets.
func (c *Context) ReadString(pref string, args ...any) string {
var text string var text string
if pref != "" { if pref != "" {
c.Sendf(pref, args...) c.Sendf(promptf, args...)
} }
for u := range c.Input() { for u := range c.Input() {
if u == nil { if u == nil {
@ -382,6 +198,10 @@ func (c *Context) ReadString(pref string, args ...any) string {
return text return text
} }
func (c Context) Update() Update {
return c.update
}
// Returns the reader for specified file ID and path. // Returns the reader for specified file ID and path.
func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) { func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
file, err := c.Bot.Api.GetFile(tgbotapi.FileConfig{FileID:string(fileId)}) file, err := c.Bot.Api.GetFile(tgbotapi.FileConfig{FileID:string(fileId)})

View file

@ -27,7 +27,7 @@ var (
// The type implements the structure to easily send // The type implements the structure to easily send
// files to the client. // files to the client.
type File struct { type File struct {
*MessageCompo MessageCompo
name string name string
reader io.Reader reader io.Reader
upload bool upload bool
@ -108,7 +108,7 @@ func (f *File) SendData() string {
func (f *File) SendConfig( func (f *File) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig) { ) (SendConfig) {
var config SendConfig var config SendConfig
cid := sid.ToApi() cid := sid.ToApi()
@ -127,5 +127,5 @@ func (f *File) SendConfig(
} }
return &config return config
} }

22
go.go Normal file
View file

@ -0,0 +1,22 @@
package tg
func Go(pth Path) UI {
return UI{
GoWidget(pth),
}
}
type GoWidget string
// Implementing the Server interface.
func (widget GoWidget) Serve(c Context) {
c.input.Close()
c.Go(Path(widget))
}
func (widget GoWidget) Render(c Context) UI {
return UI{widget}
}
func (widget GoWidget) Filter(u Update) bool {
return true
}

View file

@ -4,13 +4,14 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
// The type represents keyboard to be emdedded into the messages (inline in Telegram terms). // The type represents keyboard to be emdedded
// into the messages (inline in Telegram terms).
type Inline struct { type Inline struct {
*Keyboard Keyboard
} }
// Convert the inline keyboard to markup for the tgbotapi. // Convert the inline keyboard to markup for the tgbotapi.
func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{} rows := [][]tgbotapi.InlineKeyboardButton{}
for _, row := range kbd.Rows { for _, row := range kbd.Rows {
if row == nil { if row == nil {
@ -31,14 +32,14 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
// The type implements message with an inline keyboard. // The type implements message with an inline keyboard.
type InlineCompo struct { type InlineCompo struct {
*MessageCompo MessageCompo
*Inline Inline
} }
// Implementing the Sendable interface. // Implementing the Sendable interface.
func (compo *InlineCompo) SendConfig( func (compo *InlineCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig) { ) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot) sendConfig := compo.MessageCompo.SendConfig(sid, bot)
if len(compo.Inline.Rows) > 0 { if len(compo.Inline.Rows) > 0 {
sendConfig.Message.ReplyMarkup = compo.Inline.ToApi() sendConfig.Message.ReplyMarkup = compo.Inline.ToApi()
@ -48,7 +49,9 @@ func (compo *InlineCompo) SendConfig(
} }
// Update the component on the client side. // Update the component on the client side.
func (compo *InlineCompo) Update(c *Context) { // Requires exactly the pointer but not the value
// cause it changes insides of the structure.
func (compo *InlineCompo) Update(c Context) {
if compo.Message != nil { if compo.Message != nil {
var edit tgbotapi.Chattable var edit tgbotapi.Chattable
markup := compo.Inline.ToApi() markup := compo.Inline.ToApi()
@ -74,7 +77,7 @@ func (compo *InlineCompo) Update(c *Context) {
} }
// Implementing the Filterer interface. // Implementing the Filterer interface.
func (compo *InlineCompo) Filter(u *Update) bool { func (compo InlineCompo) Filter(u Update) bool {
if compo == nil || u.CallbackQuery == nil { if compo == nil || u.CallbackQuery == nil {
return true return true
} }
@ -88,13 +91,13 @@ func (compo *InlineCompo) Filter(u *Update) bool {
} }
// Implementing the Server interface. // Implementing the Server interface.
func (compo *InlineCompo) Serve(c *Context) { func (compo InlineCompo) Serve(c Context) {
for u := range c.Input() { for u := range c.Input() {
compo.OnOneUpdate(c, u) compo.OnOneUpdate(c, u)
} }
} }
func (compo *InlineCompo) OnOneUpdate(c *Context, u *Update) { func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
var act Action var act Action
btns := compo.ButtonMap() btns := compo.ButtonMap()
cb := tgbotapi.NewCallback( cb := tgbotapi.NewCallback(

View file

@ -14,7 +14,7 @@ type Keyboard struct {
} }
// Returns the new keyboard with specified rows. // Returns the new keyboard with specified rows.
func NewKeyboard(rows ...ButtonRow) *Keyboard { func NewKeyboard(rows ...ButtonRow) Keyboard {
ret := &Keyboard{} ret := &Keyboard{}
for _, row := range rows { for _, row := range rows {
if row != nil && len(row) > 0 { if row != nil && len(row) > 0 {
@ -24,7 +24,7 @@ func NewKeyboard(rows ...ButtonRow) *Keyboard {
return ret return ret
} }
func (kbd *Keyboard) RowNum() int { func (kbd Keyboard) RowNum() int {
return len(kbd.Rows) return len(kbd.Rows)
} }
@ -36,7 +36,7 @@ func (kbd *Keyboard) RemoveRow(i int) {
} }
// Adds a new button row to the current keyboard. // Adds a new button row to the current keyboard.
func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { func (kbd Keyboard) Row(btns ...Button) Keyboard {
// For empty row. We do not need that. // For empty row. We do not need that.
if len(btns) < 1 { if len(btns) < 1 {
return kbd return kbd
@ -56,7 +56,7 @@ func (kbd *Keyboard) Row(btns ...*Button) *Keyboard {
} }
// Adds buttons as one column list. // Adds buttons as one column list.
func (kbd *Keyboard) List(btns ...*Button) *Keyboard { func (kbd Keyboard) List(btns ...Button) Keyboard {
for _, btn := range btns { for _, btn := range btns {
if btn == nil { if btn == nil {
continue continue
@ -68,18 +68,13 @@ func (kbd *Keyboard) List(btns ...*Button) *Keyboard {
// Set the default action when no button provides // Set the default action when no button provides
// key to the data we got. // key to the data we got.
func (kbd *Keyboard) WithAction(a Action) *Keyboard { func (kbd Keyboard) WithAction(a Action) Keyboard {
kbd.Action = a kbd.Action = a
return kbd return kbd
} }
// Alias to WithAction but better typing when setting // Returns the map of buttons. Where the key
// a specific function // is button data and the value is Action.
func (kbd *Keyboard) ActionFunc(fn ActionFunc) *Keyboard {
return kbd.WithAction(fn)
}
// Returns the map of buttons.
func (kbd Keyboard) ButtonMap() ButtonMap { func (kbd Keyboard) ButtonMap() ButtonMap {
if kbd.buttonMap == nil { if kbd.buttonMap == nil {
kbd.buttonMap = kbd.MakeButtonMap() kbd.buttonMap = kbd.MakeButtonMap()
@ -101,14 +96,14 @@ func (kbd Keyboard) MakeButtonMap() ButtonMap {
} }
// Convert the keyboard to the more specific inline one. // Convert the keyboard to the more specific inline one.
func (kbd *Keyboard) Inline() *Inline { func (kbd Keyboard) Inline() Inline {
ret := &Inline{} ret := Inline{}
ret.Keyboard = kbd ret.Keyboard = kbd
return ret return ret
} }
// Convert the keyboard to the more specific reply one. // Convert the keyboard to the more specific reply one.
func (kbd *Keyboard) Reply() *Reply { func (kbd Keyboard) Reply() Reply {
ret := &Reply{} ret := &Reply{}
ret.Keyboard = kbd ret.Keyboard = kbd
// it is used more often than not once. // it is used more often than not once.

View file

@ -10,7 +10,7 @@ type Message = tgbotapi.Message
// Simple text message component type. // Simple text message component type.
type MessageCompo struct { type MessageCompo struct {
Message *Message Message Message
ParseMode string ParseMode string
Text string Text string
} }
@ -26,14 +26,19 @@ func Escape2(str string) string {
return string(escapeRe.ReplaceAll([]byte(str), []byte("\\$1"))) return string(escapeRe.ReplaceAll([]byte(str), []byte("\\$1")))
} }
func (compo *MessageCompo) Update(c *Context) { // Call the function after the message was sent.
func (compo *MessageCompo) Update(c Context) error {
edit := tgbotapi.NewEditMessageText( edit := tgbotapi.NewEditMessageText(
c.Session.Id.ToApi(), c.Session.Id.ToApi(),
compo.Message.MessageID, compo.Message.MessageID,
compo.Text, compo.Text,
) )
msg, _ := c.Bot.Api.Send(edit) msg, err := c.bot.api.Send(edit)
compo.Message = &msg if err != nil {
return err
}
compo.Message = msg
return nil
} }
func (compo *MessageCompo) Delete(c *Context) { func (compo *MessageCompo) Delete(c *Context) {
@ -44,59 +49,59 @@ func (compo *MessageCompo) Delete(c *Context) {
// Is only implemented to make it sendable and so we can put it // Is only implemented to make it sendable and so we can put it
// return of rendering functions. // return of rendering functions.
func (compo *MessageCompo) SetMessage(msg *Message) { func (compo *MessageCompo) SetMessage(msg Message) {
compo.Message = msg compo.Message = msg
} }
// Return new message with the specified text. // Return new message with the specified text.
func NewMessage(format string, v ...any) *MessageCompo { func Messagef(format string, v ...any) MessageCompo {
ret := &MessageCompo{} ret := MessageCompo{}
ret.Text = fmt.Sprintf(format, v...) ret.Text = fmt.Sprintf(format, v...)
ret.ParseMode = tgbotapi.ModeMarkdown ret.ParseMode = tgbotapi.ModeMarkdown
return ret return ret
} }
// Return message with the specified parse mode. // Return message with the specified parse mode.
func (msg *MessageCompo) withParseMode(mode string) *MessageCompo { func (msg MessageCompo) withParseMode(mode string) MessageCompo {
msg.ParseMode = mode msg.ParseMode = mode
return msg return msg
} }
// Set the default Markdown parsing mode. // Set the default Markdown parsing mode.
func (msg *MessageCompo) MD() *MessageCompo { func (msg MessageCompo) MD() MessageCompo {
return msg.withParseMode(tgbotapi.ModeMarkdown) return msg.withParseMode(tgbotapi.ModeMarkdown)
} }
// Set the Markdown 2 parsing mode. // Set the Markdown 2 parsing mode.
func (msg *MessageCompo) MD2() *MessageCompo { func (msg MessageCompo) MD2() MessageCompo {
return msg.withParseMode(tgbotapi.ModeMarkdownV2) return msg.withParseMode(tgbotapi.ModeMarkdownV2)
} }
// Set the HTML parsing mode. // Set the HTML parsing mode.
func (msg *MessageCompo) HTML() *MessageCompo { func (msg MessageCompo) HTML() MessageCompo {
return msg.withParseMode(tgbotapi.ModeHTML) return msg.withParseMode(tgbotapi.ModeHTML)
} }
// Transform the message component into one with reply keyboard. // Transform the message component into one with reply keyboard.
func (msg *MessageCompo) Inline(inline *Inline) *InlineCompo { func (msg MessageCompo) Inline(inline Inline) InlineCompo {
return &InlineCompo{ return InlineCompo{
Inline: inline, Inline: inline,
MessageCompo: msg, MessageCompo: msg,
} }
} }
// Transform the message component into one with reply keyboard. // Transform the message component into one with reply keyboard.
func (msg *MessageCompo) Reply(reply *Reply) *ReplyCompo { func (msg MessageCompo) Reply(reply Reply) ReplyCompo {
return &ReplyCompo{ return ReplyCompo{
Reply: reply, Reply: reply,
MessageCompo: msg, MessageCompo: msg,
} }
} }
// Transform the message component into the location one. // Transform the message component into the location one.
func (msg *MessageCompo) Location( func (msg MessageCompo) Location(
lat, long float64, lat, long float64,
) *LocationCompo { ) LocationCompo {
ret := &LocationCompo{ ret := &LocationCompo{
MessageCompo: msg, MessageCompo: msg,
Location: Location{ Location: Location{
@ -108,9 +113,9 @@ func (msg *MessageCompo) Location(
} }
// Implementing the Sendable interface. // Implementing the Sendable interface.
func (config *MessageCompo) SendConfig( func (config MessageCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig) { ) (SendConfig) {
var ( var (
ret SendConfig ret SendConfig
text string text string
@ -122,20 +127,18 @@ func (config *MessageCompo) SendConfig(
text = config.Text text = config.Text
} }
//text = strings.ReplaceAll(text, "-", "\\-")
msg := tgbotapi.NewMessage(sid.ToApi(), text) msg := tgbotapi.NewMessage(sid.ToApi(), text)
ret.Message = &msg msg.ParseMode = config.ParseMode
ret.Message.ParseMode = config.ParseMode ret.Chattable = msg
return &ret return ret
} }
// Empty serving to use messages in rendering. // Empty serving to use messages in rendering.
func (compo *MessageCompo) Serve(c *Context) {} func (compo *MessageCompo) Serve(c Context) {}
// Filter that skips everything. Messages cannot do anything with updates. // Filter that skips everything. Messages cannot do anything with updates.
func (compo *MessageCompo) Filter(_ *Update) bool { func (compo *MessageCompo) Filter(_ Update) bool {
// Skip everything // Skip everything
return true return true
} }

View file

@ -6,7 +6,7 @@ import (
// The type represents reply keyboards. // The type represents reply keyboards.
type Reply struct { type Reply struct {
*Keyboard Keyboard
// If true will be removed after one press. // If true will be removed after one press.
OneTime bool OneTime bool
// If true will remove the keyboard on send. // If true will remove the keyboard on send.
@ -15,20 +15,20 @@ type Reply struct {
// Set if we should remove current keyboard on the user side // Set if we should remove current keyboard on the user side
// when sending the keyboard. // when sending the keyboard.
func (kbd *Reply) WithRemove(remove bool) *Reply { func (kbd Reply) WithRemove(remove bool) Reply {
kbd.Remove = remove kbd.Remove = remove
return kbd return kbd
} }
// Set if the keyboard should be hidden after // Set if the keyboard should be hidden after
// one of buttons is pressede. // one of buttons is pressede.
func (kbd *Reply) WithOneTime(oneTime bool) *Reply{ func (kbd Reply) WithOneTime(oneTime bool) Reply{
kbd.OneTime = oneTime kbd.OneTime = oneTime
return kbd return kbd
} }
// Convert the Keyboard to the Telegram API type of reply keyboard. // Convert the Keyboard to the Telegram API type of reply keyboard.
func (kbd *Reply) ToApi() any { func (kbd Reply) ToApi() any {
// Shades everything. // Shades everything.
if kbd.Remove { if kbd.Remove {
return tgbotapi.NewRemoveKeyboard(true) return tgbotapi.NewRemoveKeyboard(true)
@ -58,20 +58,21 @@ func (kbd *Reply) ToApi() any {
// The type implements reply keyboard widget. // The type implements reply keyboard widget.
type ReplyCompo struct { type ReplyCompo struct {
*MessageCompo MessageCompo
*Reply Reply
} }
// Implementing the sendable interface. // Implementing the sendable interface.
func (compo *ReplyCompo) SendConfig( func (compo ReplyCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig) { ) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot) sendConfig := compo.MessageCompo.SendConfig(sid, bot)
sendConfig.Message.ReplyMarkup = compo.Reply.ToApi() sendConfig.Message.ReplyMarkup = compo.Reply.ToApi()
return sendConfig return sendConfig
} }
func (compo *ReplyCompo) Filter( // Implementing the Server interface.
func (compo ReplyCompo) Filter(
u *Update, u *Update,
) bool { ) bool {
if compo == nil || u.Message == nil { if compo == nil || u.Message == nil {
@ -93,7 +94,7 @@ func (compo *ReplyCompo) Filter(
} }
// Implementing the UI interface. // Implementing the UI interface.
func (compo *ReplyCompo) Serve(c *Context) { func (compo ReplyCompo) Serve(c *Context) {
for u := range c.Input() { for u := range c.Input() {
var btn *Button var btn *Button
text := u.Message.Text text := u.Message.Text

30
send.go
View file

@ -10,24 +10,14 @@ type MessageId int64
// way to define what message will be // way to define what message will be
// sent to the side of a user. // sent to the side of a user.
type Sendable interface { type Sendable interface {
SendConfig(SessionId, *Bot) (*SendConfig) SendConfig(SessionId, *Bot) (SendConfig)
SetMessage(*Message) SetMessage(Message)
}
type Errorer interface {
Err() error
} }
// The type is used as an endpoint to send messages // The type is used as an endpoint to send messages
// via bot.Send . // via bot.Send .
type SendConfig struct { type SendConfig struct {
// Message with text and keyboard. Chattable tgbotapi.Chattable
Message *tgbotapi.MessageConfig
// The image to be sent.
Photo *tgbotapi.PhotoConfig
Document *tgbotapi.DocumentConfig
Location *tgbotapi.LocationConfig
Error error Error error
} }
@ -35,17 +25,7 @@ type SendConfig struct {
type MessageMap map[string] *Message type MessageMap map[string] *Message
// Convert to the bot.Api.Send format. // Convert to the bot.Api.Send format.
func (config *SendConfig) ToApi() tgbotapi.Chattable { func (config SendConfig) ToApi() tgbotapi.Chattable {
switch { return config.Chattable
case config.Message != nil :
return *(config.Message)
case config.Photo != nil :
return *(config.Photo)
case config.Location != nil :
return *(config.Location)
case config.Document != nil :
return *(config.Document)
}
return nil
} }

View file

@ -1,5 +1,16 @@
package tg package tg
// The type represents map of sessions using
// as key.
type SessionMap map[SessionId]*Session
// Add new empty session by it's ID.
func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session {
ret := NewSession(sid, scope)
sm[sid] = ret
return ret
}
// The way to determine where the context is // The way to determine where the context is
// related to. // related to.
type SessionScope uint8 type SessionScope uint8
@ -27,6 +38,11 @@ type Session struct {
Scope SessionScope Scope SessionScope
// Custom value for each user. // Custom value for each user.
Data any Data any
bot *Bot
pathHistory []Path
skippedUpdates *UpdateChan
updates *UpdateChan
} }
// Return new empty session with specified user ID. // Return new empty session with specified user ID.
@ -37,14 +53,129 @@ func NewSession(id SessionId, scope SessionScope) *Session {
} }
} }
// The type represents map of sessions using // Changes screen of user to the Id one.
// as key. func (s *Session) go_(pth Path, arg any) error {
type SessionMap map[SessionId]*Session var err error
if pth == "" {
s.pathHistory = []Path{}
return nil
}
var back bool
if pth == "-" {
if len(s.pathHistory) < 2 {
return s.Go("")
}
pth = s.pathHistory[len(s.pathHistory)-2]
s.pathHistory = s.pathHistory[:len(s.pathHistory)-1]
}
// Getting the screen and changing to
// then executing its widget.
if !pth.IsAbs() {
pth = (s.Path() + "/" + pth).Clean()
}
// Add new empty session by it's ID. if !s.PathExist(pth) {
func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session { return ScreenNotExistErr
ret := NewSession(sid, scope) }
sm[sid] = ret
return ret if !back && s.Path() != pth {
s.pathHistory = append(s.pathHistory, pth)
}
// Stopping the current widget.
screen := s.bot.behaviour.Screens[pth]
s.skippedUpdates.Close()
if screen.Widget != nil {
s.skippedUpdates, err = s.runWidget(screen.Widget, arg)
if err != nil {
return err
}
} else {
return NoWidgetForScreenErr
}
return nil
} }
func (s *Session) runCompo(compo Component, arg any) (*UpdateChan, error) {
if compo == nil {
return nil, nil
}
s, ok := compo.(Sendable)
if ok {
msg, err := c.Send(s)
if err != nil {
return nil, err
}
s.SetMessage(msg)
}
updates := NewUpdateChan()
go func() {
compo.Serve(
c.WithInput(updates).
WithArg(arg),
)
// To let widgets finish themselves before
// the channel is closed and close it by themselves.
updates.Close()
}()
return updates, nil
}
// Run widget in background returning the new input channel for it.
func (c *Context) runWidget(widget Widget, arg any) (*UpdateChan, error) {
var err error
if widget == nil {
return nil, EmptyWidgetErr
}
pth := c.Path()
compos := widget.Render(c.WithArg(c.makeArg(args)))
// Leave if changed path or components are empty.
if compos == nil || pth != c.Path() {
return nil, EmptyCompoErr
}
chns := make([]*UpdateChan, len(compos))
for i, compo := range compos {
chns[i], err = c.runCompo(compo, arg)
if err != nil {
for _, chn := range chns {
chn.Close()
}
return nil, err
}
}
ret := NewUpdateChan()
go func() {
ln := len(compos)
UPDATE:
for u := range ret.Chan() {
if u == nil {
break
}
cnt := 0
for i, compo := range compos {
chn := chns[i]
if chn.Closed() {
cnt++
continue
}
if !compo.Filter(u) {
chn.Send(u)
continue UPDATE
}
}
if cnt == ln {
break
}
}
ret.Close()
for _, chn := range chns {
chn.Close()
}
}()
return ret, nil
}

View file

@ -5,13 +5,12 @@ import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
type FileId string type FileId string
type Update struct { type Update struct {
*tgbotapi.Update tgbotapi.Update
c *Context
} }
// The type represents general update channel. // The type represents general update channel.
type UpdateChan struct { type UpdateChan struct {
chn chan *Update chn chan Update
} }
// Return new update channel. // Return new update channel.
@ -60,13 +59,13 @@ func (updates *UpdateChan) Close() {
close(chn) close(chn)
} }
func (u *Update) HasDocument() bool { func (u Update) HasDocument() bool {
return u != nil && return u != nil &&
u.Message != nil && u.Message != nil &&
u.Message.Document != nil u.Message.Document != nil
} }
func (u *Update) DocumentId() FileId { func (u Update) DocumentId() FileId {
return FileId(u.Update.Message.Document.FileID) return FileId(u.Update.Message.Document.FileID)
} }
@ -74,23 +73,24 @@ func (u *Update) DocumentName() string {
return u.Message.Document.FileName return u.Message.Document.FileName
} }
func (u *Update) DocumentSize() int { func (u Update) DocumentSize() int {
return u.Message.Document.FileSize return u.Message.Document.FileSize
} }
func (u *Update) DocumentMimeType() string { func (u Update) DocumentMimeType() string {
return u.Message.Document.MimeType return u.Message.Document.MimeType
} }
func (u *Update) HasPhotos() bool { func (u Update) HasPhotos() bool {
return u.Message != nil && u.Message.Photo != nil && return u.Message != nil && u.Message.Photo != nil &&
len(u.Message.Photo) != 0 len(u.Message.Photo) != 0
} }
func (u *Update) PhotoIds() []FileId { func (u Update) PhotoIds() []FileId {
ret := make([]FileId, len(u.Message.Photo)) ret := make([]FileId, len(u.Message.Photo))
for i, photo := range u.Message.Photo { for i, photo := range u.Message.Photo {
ret[i] = FileId(photo.FileID) ret[i] = FileId(photo.FileID)
} }
return ret return ret
} }