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

View file

@ -16,7 +16,7 @@ type Button struct {
Action Action
}
type ButtonMap map[string]*Button
type ButtonMap map[string]Button
// Returns the only location button in the map.
func (btnMap ButtonMap) LocationButton() *Button {
@ -32,14 +32,14 @@ func (btnMap ButtonMap) LocationButton() *Button {
type ButtonRow []*Button
// 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{
Text: fmt.Sprintf(format, v...),
}
}
// Randomize buttons data to make the key unique.
func (btn *Button) Rand() *Button {
func (btn Button) Rand() Button {
rData := make([]byte, 8)
rand.Read(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.
func (btn *Button) WithUrl(format string, v ...any) *Button {
func (btn Button) WithUrl(format string, v ...any) Button {
btn.Url = fmt.Sprintf(format, v...)
return btn
}
// Set the action when pressing the button.
// By default is nil and does nothing.
func (btn *Button) WithAction(a Action) *Button {
func (btn Button) WithAction(a Action) Button {
btn.Action = a
return btn
}
func (btn *Button) WithData(dat string) *Button {
func (btn Button) WithData(dat string) Button {
btn.Data = dat
return btn
}
// 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
return btn
}
func (btn *Button) ActionFunc(fn ActionFunc) *Button {
return btn.WithAction(fn)
}
func (btn *Button) Go(pth Path, args ...any) *Button {
func (btn Button) Go(pth Path, args ...any) Button {
return btn.WithAction(ScreenGo{
Path: pth,
Args: args,
})
}
func (btn *Button) ToTelegram() apix.KeyboardButton {
func (btn Button) ToTelegram() apix.KeyboardButton {
ret := apix.NewKeyboardButton(btn.Text)
if btn.SendLocation {
ret.RequestLocation = true
@ -91,7 +87,7 @@ func (btn *Button) ToTelegram() apix.KeyboardButton {
return ret
}
func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton {
func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
if 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.
func (btn *Button) Key() string {
func (btn Button) Key() string {
if btn == nil {
return ""
}

View file

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

View file

@ -8,57 +8,17 @@ import (
//"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.
type Context struct {
*context
session *Session
// The update that called the Context usage.
*Update
update Update
// Used as way to provide outer values redirection
// into widgets and actions. It is like arguments
// for REST API request etc.
arg any
typ ContextType
// Instead of updates as argument.
input *UpdateChan
}
@ -68,7 +28,7 @@ type Context struct {
// maybe you will find another usage for this.
// Returns users context by specified session ID
// 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]
if !ok {
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
// and components themselves.
type Func func(*Context)
func (f Func) Act(c *Context) {
type Func func(Context)
func (f Func) Act(c Context) {
f(c)
}
func (f Func) Serve(c *Context) {
func (f Func) Serve(c Context) {
f(c)
}
func(f Func) Filter(_ *Update) bool {
@ -108,13 +64,13 @@ const (
)
// Goroutie function to handle each user.
func (c *Context) serve() {
func (c Context) serve() {
beh := c.Bot.behaviour
c.Run(beh.Init)
beh.Root.Serve(c)
}
func (c *Context) Path() Path {
func (c Context) Path() Path {
ln := len(c.pathHistory)
if ln == 0 {
return ""
@ -122,11 +78,11 @@ func (c *Context) Path() Path {
return c.pathHistory[ln-1]
}
func (c *Context) Arg() any {
func (c Context) Arg() any {
return c.arg
}
func (c *Context) Run(a Action) {
func (c Context) Run(a Action) {
if a != nil {
a.Act(c)
}
@ -135,12 +91,12 @@ func (c *Context) Run(a Action) {
// Only for the root widget usage.
// Skip the update sending it down to
// the underlying widget.
func (c *Context) Skip(u *Update) {
func (c Context) Skip(u Update) {
c.skippedUpdates.Send(u)
}
// 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)
if config.Error != nil {
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
// 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...))
}
// 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())
}
// 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())
}
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())
}
// Get the input for current widget.
// Should be used inside handlers (aka "Serve").
func (c *Context) Input() chan *Update {
func (c Context) Input() chan Update {
return c.input.Chan()
}
// Returns copy of current context so
// 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()
func (c Context) WithArg(v any) Context {
c.arg = v
return c
}
func (c *Context) WithUpdate(u *Update) *Context {
c = c.Copy()
func (c Context) WithUpdate(u *Update) Context {
c.Update = u
return c
}
func (c *Context) WithInput(input *UpdateChan) *Context {
c = c.Copy()
func (c Context) WithInput(input *UpdateChan) Context {
c.input = input
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.
type Action interface {
Act(*Context)
Act(Context)
}
type ActionFunc func(*Context)
type ActionFunc func(Context)
func (af ActionFunc) Act(c *Context) {
af(c)
}
func (c *Context) History() []Path {
return c.pathHistory
func (c Context) History() []Path {
return c.session.pathHistory
}
// Changes screen of user to the Id one.
func (c *Context) Go(pth Path, args ...any) error {
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 {
return c.bot.behaviour.PathExist(pth)
}
func (c *Context) PathExist(pth Path) bool {
return c.Bot.behaviour.PathExist(pth)
}
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 {
// Simple way to read strings for widgets with
// the specified prompt.
func (c Context) ReadString(promptf string, args ...any) string {
var text string
if pref != "" {
c.Sendf(pref, args...)
c.Sendf(promptf, args...)
}
for u := range c.Input() {
if u == nil {
@ -382,6 +198,10 @@ func (c *Context) ReadString(pref string, args ...any) string {
return text
}
func (c Context) Update() Update {
return c.update
}
// Returns the reader for specified file ID and path.
func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
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
// files to the client.
type File struct {
*MessageCompo
MessageCompo
name string
reader io.Reader
upload bool
@ -108,7 +108,7 @@ func (f *File) SendData() string {
func (f *File) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig) {
) (SendConfig) {
var config SendConfig
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"
)
// 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 {
*Keyboard
Keyboard
}
// Convert the inline keyboard to markup for the tgbotapi.
func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{}
for _, row := range kbd.Rows {
if row == nil {
@ -31,14 +32,14 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
// The type implements message with an inline keyboard.
type InlineCompo struct {
*MessageCompo
*Inline
MessageCompo
Inline
}
// Implementing the Sendable interface.
func (compo *InlineCompo) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig) {
) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
if len(compo.Inline.Rows) > 0 {
sendConfig.Message.ReplyMarkup = compo.Inline.ToApi()
@ -48,7 +49,9 @@ func (compo *InlineCompo) SendConfig(
}
// 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 {
var edit tgbotapi.Chattable
markup := compo.Inline.ToApi()
@ -74,7 +77,7 @@ func (compo *InlineCompo) Update(c *Context) {
}
// Implementing the Filterer interface.
func (compo *InlineCompo) Filter(u *Update) bool {
func (compo InlineCompo) Filter(u Update) bool {
if compo == nil || u.CallbackQuery == nil {
return true
}
@ -88,13 +91,13 @@ func (compo *InlineCompo) Filter(u *Update) bool {
}
// Implementing the Server interface.
func (compo *InlineCompo) Serve(c *Context) {
func (compo InlineCompo) Serve(c Context) {
for u := range c.Input() {
compo.OnOneUpdate(c, u)
}
}
func (compo *InlineCompo) OnOneUpdate(c *Context, u *Update) {
func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
var act Action
btns := compo.ButtonMap()
cb := tgbotapi.NewCallback(

View file

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

View file

@ -10,7 +10,7 @@ type Message = tgbotapi.Message
// Simple text message component type.
type MessageCompo struct {
Message *Message
Message Message
ParseMode string
Text string
}
@ -26,14 +26,19 @@ func Escape2(str string) string {
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(
c.Session.Id.ToApi(),
compo.Message.MessageID,
compo.Text,
)
msg, _ := c.Bot.Api.Send(edit)
compo.Message = &msg
msg, err := c.bot.api.Send(edit)
if err != nil {
return err
}
compo.Message = msg
return nil
}
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
// return of rendering functions.
func (compo *MessageCompo) SetMessage(msg *Message) {
func (compo *MessageCompo) SetMessage(msg Message) {
compo.Message = msg
}
// Return new message with the specified text.
func NewMessage(format string, v ...any) *MessageCompo {
ret := &MessageCompo{}
func Messagef(format string, v ...any) MessageCompo {
ret := MessageCompo{}
ret.Text = fmt.Sprintf(format, v...)
ret.ParseMode = tgbotapi.ModeMarkdown
return ret
}
// Return message with the specified parse mode.
func (msg *MessageCompo) withParseMode(mode string) *MessageCompo {
func (msg MessageCompo) withParseMode(mode string) MessageCompo {
msg.ParseMode = mode
return msg
}
// Set the default Markdown parsing mode.
func (msg *MessageCompo) MD() *MessageCompo {
func (msg MessageCompo) MD() MessageCompo {
return msg.withParseMode(tgbotapi.ModeMarkdown)
}
// Set the Markdown 2 parsing mode.
func (msg *MessageCompo) MD2() *MessageCompo {
func (msg MessageCompo) MD2() MessageCompo {
return msg.withParseMode(tgbotapi.ModeMarkdownV2)
}
// Set the HTML parsing mode.
func (msg *MessageCompo) HTML() *MessageCompo {
func (msg MessageCompo) HTML() MessageCompo {
return msg.withParseMode(tgbotapi.ModeHTML)
}
// Transform the message component into one with reply keyboard.
func (msg *MessageCompo) Inline(inline *Inline) *InlineCompo {
return &InlineCompo{
func (msg MessageCompo) Inline(inline Inline) InlineCompo {
return InlineCompo{
Inline: inline,
MessageCompo: msg,
}
}
// Transform the message component into one with reply keyboard.
func (msg *MessageCompo) Reply(reply *Reply) *ReplyCompo {
return &ReplyCompo{
func (msg MessageCompo) Reply(reply Reply) ReplyCompo {
return ReplyCompo{
Reply: reply,
MessageCompo: msg,
}
}
// Transform the message component into the location one.
func (msg *MessageCompo) Location(
func (msg MessageCompo) Location(
lat, long float64,
) *LocationCompo {
) LocationCompo {
ret := &LocationCompo{
MessageCompo: msg,
Location: Location{
@ -108,9 +113,9 @@ func (msg *MessageCompo) Location(
}
// Implementing the Sendable interface.
func (config *MessageCompo) SendConfig(
func (config MessageCompo) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig) {
) (SendConfig) {
var (
ret SendConfig
text string
@ -122,20 +127,18 @@ func (config *MessageCompo) SendConfig(
text = config.Text
}
//text = strings.ReplaceAll(text, "-", "\\-")
msg := tgbotapi.NewMessage(sid.ToApi(), text)
ret.Message = &msg
ret.Message.ParseMode = config.ParseMode
msg.ParseMode = config.ParseMode
ret.Chattable = msg
return &ret
return ret
}
// 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.
func (compo *MessageCompo) Filter(_ *Update) bool {
func (compo *MessageCompo) Filter(_ Update) bool {
// Skip everything
return true
}

View file

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

30
send.go
View file

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

View file

@ -1,5 +1,16 @@
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
// related to.
type SessionScope uint8
@ -27,6 +38,11 @@ type Session struct {
Scope SessionScope
// Custom value for each user.
Data any
bot *Bot
pathHistory []Path
skippedUpdates *UpdateChan
updates *UpdateChan
}
// 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
// as key.
type SessionMap map[SessionId]*Session
// Changes screen of user to the Id one.
func (s *Session) go_(pth Path, arg any) error {
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.
func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session {
ret := NewSession(sid, scope)
sm[sid] = ret
return ret
if !s.PathExist(pth) {
return ScreenNotExistErr
}
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 Update struct {
*tgbotapi.Update
c *Context
tgbotapi.Update
}
// The type represents general update channel.
type UpdateChan struct {
chn chan *Update
chn chan Update
}
// Return new update channel.
@ -60,13 +59,13 @@ func (updates *UpdateChan) Close() {
close(chn)
}
func (u *Update) HasDocument() bool {
func (u Update) HasDocument() bool {
return u != nil &&
u.Message != nil &&
u.Message.Document != nil
}
func (u *Update) DocumentId() FileId {
func (u Update) DocumentId() FileId {
return FileId(u.Update.Message.Document.FileID)
}
@ -74,23 +73,24 @@ func (u *Update) DocumentName() string {
return u.Message.Document.FileName
}
func (u *Update) DocumentSize() int {
func (u Update) DocumentSize() int {
return u.Message.Document.FileSize
}
func (u *Update) DocumentMimeType() string {
func (u Update) DocumentMimeType() string {
return u.Message.Document.MimeType
}
func (u *Update) HasPhotos() bool {
func (u Update) HasPhotos() bool {
return u.Message != nil && u.Message.Photo != nil &&
len(u.Message.Photo) != 0
}
func (u *Update) PhotoIds() []FileId {
func (u Update) PhotoIds() []FileId {
ret := make([]FileId, len(u.Message.Photo))
for i, photo := range u.Message.Photo {
ret[i] = FileId(photo.FileID)
}
return ret
}