Re-implemented the customizing by the Widget interface.
This commit is contained in:
parent
5b00189ea2
commit
b2ce5fe2ea
7 changed files with 400 additions and 293 deletions
155
cmd/test/main.go
155
cmd/test/main.go
|
@ -16,11 +16,31 @@ type UserData struct {
|
|||
Counter int
|
||||
}
|
||||
|
||||
type MutateMessageWidget struct {
|
||||
Mutate func(string) string
|
||||
}
|
||||
|
||||
func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
|
||||
ret := &MutateMessageWidget{}
|
||||
ret.Mutate = fn
|
||||
return ret
|
||||
}
|
||||
|
||||
func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) {
|
||||
for u := range updates {
|
||||
if u.Message == nil {
|
||||
continue
|
||||
}
|
||||
text := u.Message.Text
|
||||
c.Sendf("%s", w.Mutate(text))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
startScreenButton = tg.NewButton("🏠 To the start screen").
|
||||
ScreenChange("start")
|
||||
|
||||
incDecKeyboard = tg.NewInline().Row(
|
||||
incDecKeyboard = tg.NewReply().Row(
|
||||
tg.NewButton("+").ActionFunc(func(c *tg.Context) {
|
||||
d := c.Session.Value.(*UserData)
|
||||
d.Counter++
|
||||
|
@ -38,12 +58,12 @@ var (
|
|||
navKeyboard = tg.NewReply().
|
||||
WithOneTime(true).
|
||||
Row(
|
||||
tg.NewButton("Inc/Dec").ScreenChange("inc/dec"),
|
||||
tg.NewButton("Inc/Dec").ScreenChange("start/inc-dec"),
|
||||
).Row(
|
||||
tg.NewButton("Upper case").ScreenChange("upper-case"),
|
||||
tg.NewButton("Lower case").ScreenChange("lower-case"),
|
||||
tg.NewButton("Upper case").ScreenChange("start/upper-case"),
|
||||
tg.NewButton("Lower case").ScreenChange("start/lower-case"),
|
||||
).Row(
|
||||
tg.NewButton("Send location").ScreenChange("send-location"),
|
||||
tg.NewButton("Send location").ScreenChange("start/send-location"),
|
||||
)
|
||||
|
||||
sendLocationKeyboard = tg.NewReply().
|
||||
|
@ -64,7 +84,7 @@ var (
|
|||
l.Heading,
|
||||
)
|
||||
} else {
|
||||
_, err = c.Sendf("Somehow wrong location was sent")
|
||||
_, err = c.Sendf("Somehow location was not sent")
|
||||
}
|
||||
if err != nil {
|
||||
c.Sendf("%q", err)
|
||||
|
@ -89,62 +109,69 @@ var beh = tg.NewBehaviour().
|
|||
WithPreStartFunc(func(c *tg.Context){
|
||||
c.Sendf("Please, use the /start command to start the bot")
|
||||
}).WithScreens(
|
||||
tg.NewScreen("start").
|
||||
WithText(
|
||||
"The bot started!"+
|
||||
" The bot is supposed to provide basic"+
|
||||
" understand of how the API works, so just"+
|
||||
" horse around a bit to guess everything out"+
|
||||
" by yourself!",
|
||||
).WithReply(navKeyboard).
|
||||
// The inline keyboard with link to GitHub page.
|
||||
WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton("GoT Github page").
|
||||
WithUrl("https://github.com/mojosa-software/got"),
|
||||
tg.NewScreen("start", tg.NewPage(
|
||||
"The bot started!",
|
||||
).WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton("GoT Github page").
|
||||
WithUrl("https://github.com/mojosa-software/got"),
|
||||
),
|
||||
).WithReply(
|
||||
navKeyboard,
|
||||
),
|
||||
),
|
||||
tg.NewScreen("start/inc-dec", tg.NewPage(
|
||||
"The screen shows how "+
|
||||
"user separated data works "+
|
||||
"by saving the counter for each of users "+
|
||||
"separately. ",
|
||||
).WithReply(
|
||||
incDecKeyboard,
|
||||
).ActionFunc(func(c *tg.Context) {
|
||||
// The function will be calleb before serving page.
|
||||
d := c.Session.Value.(*UserData)
|
||||
c.Sendf("Current counter value = %d", d.Counter)
|
||||
}),
|
||||
),
|
||||
|
||||
tg.NewScreen("start/upper-case", tg.NewPage(
|
||||
"Type text and the bot will send you the upper case version to you",
|
||||
).WithReply(
|
||||
navToStartKeyboard,
|
||||
).WithSub(
|
||||
NewMutateMessageWidget(strings.ToUpper),
|
||||
),
|
||||
),
|
||||
|
||||
tg.NewScreen("inc/dec").
|
||||
WithText(
|
||||
"The screen shows how "+
|
||||
"user separated data works "+
|
||||
"by saving the counter for each of users "+
|
||||
"separately. ",
|
||||
).
|
||||
WithReply(&tg.ReplyKeyboard{Keyboard: incDecKeyboard.Keyboard}).
|
||||
// The function will be called when reaching the screen.
|
||||
ActionFunc(func(c *tg.Context) {
|
||||
d := c.Session.Value.(*UserData)
|
||||
c.Sendf("Current counter value = %d", d.Counter)
|
||||
}),
|
||||
tg.NewScreen("start/lower-case", tg.NewPage(
|
||||
"Type text and the bot will send you the lower case version",
|
||||
).WithReply(
|
||||
navToStartKeyboard,
|
||||
).WithSub(
|
||||
NewMutateMessageWidget(strings.ToLower),
|
||||
),
|
||||
),
|
||||
|
||||
tg.NewScreen("upper-case").
|
||||
WithText("Type text and the bot will send you the upper case version to you").
|
||||
WithReply(navToStartKeyboard).
|
||||
ActionFunc(mutateMessage(strings.ToUpper)),
|
||||
|
||||
tg.NewScreen("lower-case").
|
||||
WithText("Type text and the bot will send you the lower case version").
|
||||
WithReply(navToStartKeyboard).
|
||||
ActionFunc(mutateMessage(strings.ToLower)),
|
||||
|
||||
tg.NewScreen("send-location").
|
||||
WithText("Send your location and I will tell where you are!").
|
||||
WithReply(sendLocationKeyboard).
|
||||
WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton("Check").
|
||||
WithData("check").
|
||||
ActionFunc(func(a *tg.Context) {
|
||||
d := a.Session.Value.(*UserData)
|
||||
a.Sendf("Counter = %d", d.Counter)
|
||||
tg.NewScreen("start/send-location", tg.NewPage(
|
||||
"Send your location and I will tell where you are!",
|
||||
).WithReply(
|
||||
sendLocationKeyboard,
|
||||
).WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton(
|
||||
"Check",
|
||||
).WithData(
|
||||
"check",
|
||||
).ActionFunc(func(c *tg.Context) {
|
||||
d := c.Session.Value.(*UserData)
|
||||
c.Sendf("Counter = %d", d.Counter)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
).WithCommands(
|
||||
tg.NewCommand("start").
|
||||
Desc("start the bot").
|
||||
Desc("start or restart the bot or move to the start screen").
|
||||
ActionFunc(func(c *tg.Context){
|
||||
c.ChangeScreen("start")
|
||||
}),
|
||||
|
@ -156,12 +183,12 @@ var beh = tg.NewBehaviour().
|
|||
tg.NewCommand("read").
|
||||
Desc("reads a string and sends it back").
|
||||
ActionFunc(func(c *tg.Context) {
|
||||
c.Sendf("Type some text:")
|
||||
/*c.Sendf("Type some text:")
|
||||
msg, err := c.ReadTextMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Sendf("You typed %q", msg)
|
||||
c.Sendf("You typed %q", msg)*/
|
||||
}),
|
||||
tg.NewCommand("image").
|
||||
Desc("sends a sample image").
|
||||
|
@ -177,24 +204,6 @@ var beh = tg.NewBehaviour().
|
|||
}),
|
||||
)
|
||||
|
||||
func mutateMessage(fn func(string) string) tg.ActionFunc {
|
||||
return func(c *tg.Context) {
|
||||
for {
|
||||
msg, err := c.ReadTextMessage()
|
||||
if err == tg.NotAvailableErr {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = c.Sendf("%s", fn(msg))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gBeh = tg.NewGroupBehaviour().
|
||||
InitFunc(func(c *tg.GC) {
|
||||
}).
|
||||
|
|
|
@ -187,7 +187,6 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
|||
ctx := &context{
|
||||
Bot: bot,
|
||||
Session: session,
|
||||
updates: make(chan *Update),
|
||||
}
|
||||
chn := make(chan *Update)
|
||||
chans[sid] = chn
|
||||
|
@ -201,7 +200,6 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
|||
ctx := &context{
|
||||
Bot: bot,
|
||||
Session: lsession,
|
||||
updates: make(chan *Update),
|
||||
}
|
||||
chn := make(chan *Update)
|
||||
chans[sid] = chn
|
||||
|
|
|
@ -6,9 +6,13 @@ import (
|
|||
|
||||
// The general keyboard type used both in Reply and Inline.
|
||||
type Keyboard struct {
|
||||
// The action is called if there is no
|
||||
// defined action for the button.
|
||||
Action *action
|
||||
Rows []ButtonRow
|
||||
}
|
||||
|
||||
// The type represents reply keyboards.
|
||||
type ReplyKeyboard struct {
|
||||
Keyboard
|
||||
// If true will be removed after one press.
|
||||
|
@ -17,16 +21,18 @@ type ReplyKeyboard struct {
|
|||
Remove bool
|
||||
}
|
||||
|
||||
// The keyboard to be emdedded into the messages.
|
||||
// The type represents keyboard to be emdedded into the messages.
|
||||
type InlineKeyboard struct {
|
||||
Keyboard
|
||||
}
|
||||
|
||||
// Returns new empty inline keyboard.
|
||||
func NewInline() *InlineKeyboard {
|
||||
ret := &InlineKeyboard{}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Returns new empty reply keyboard.
|
||||
func NewReply() *ReplyKeyboard {
|
||||
ret := &ReplyKeyboard {}
|
||||
return ret
|
||||
|
@ -41,6 +47,18 @@ func (kbd *InlineKeyboard) Row(btns ...*Button) *InlineKeyboard {
|
|||
kbd.Rows = append(kbd.Rows, btns)
|
||||
return kbd
|
||||
}
|
||||
|
||||
// Set default action for the buttons in keyboard.
|
||||
func (kbd *InlineKeyboard) WithAction(a Action) *InlineKeyboard {
|
||||
kbd.Action = newAction(a)
|
||||
return kbd
|
||||
}
|
||||
|
||||
// Alias to WithAction to simpler define actions.
|
||||
func (kbd *InlineKeyboard) ActionFunc(fn ActionFunc) *InlineKeyboard {
|
||||
return kbd.WithAction(fn)
|
||||
}
|
||||
|
||||
// Adds a new button row to the current keyboard.
|
||||
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
||||
// For empty row. We do not need that.
|
||||
|
@ -51,6 +69,17 @@ func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
|||
return kbd
|
||||
}
|
||||
|
||||
// Set default action for the keyboard.
|
||||
func (kbd *ReplyKeyboard) WithAction(a Action) *ReplyKeyboard {
|
||||
kbd.Action = newAction(a)
|
||||
return kbd
|
||||
}
|
||||
|
||||
// Alias to WithAction for simpler callback declarations.
|
||||
func (kbd *ReplyKeyboard) ActionFunc(fn ActionFunc) *ReplyKeyboard {
|
||||
return kbd.WithAction(fn)
|
||||
}
|
||||
|
||||
// Convert the Keyboard to the Telegram API type of reply keyboard.
|
||||
func (kbd *ReplyKeyboard) ToApi() any {
|
||||
// Shades everything.
|
||||
|
@ -74,6 +103,7 @@ func (kbd *ReplyKeyboard) ToApi() any {
|
|||
return tgbotapi.NewReplyKeyboard(rows...)
|
||||
}
|
||||
|
||||
// Convert the inline keyboard to markup for the tgbotapi.
|
||||
func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
|
||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
for _, row := range kbd.Rows {
|
||||
|
@ -87,18 +117,22 @@ func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
|
|||
return tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
}
|
||||
|
||||
// Set if we should remove current keyboard on the user side
|
||||
// when sending the keyboard.
|
||||
func (kbd *ReplyKeyboard) WithRemove(remove bool) *ReplyKeyboard {
|
||||
kbd.Remove = remove
|
||||
return kbd
|
||||
}
|
||||
|
||||
// Set if the keyboard should be hidden after
|
||||
// one of buttons is pressede.
|
||||
func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
|
||||
kbd.OneTime = oneTime
|
||||
return kbd
|
||||
}
|
||||
|
||||
// Returns the map of buttons. Used to define the Action.
|
||||
func (kbd Keyboard) buttonMap() ButtonMap {
|
||||
func (kbd Keyboard) ButtonMap() ButtonMap {
|
||||
ret := make(ButtonMap)
|
||||
for _, vi := range kbd.Rows {
|
||||
for _, vj := range vi {
|
||||
|
@ -108,3 +142,4 @@ func (kbd Keyboard) buttonMap() ButtonMap {
|
|||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
|
186
tg/private.go
186
tg/private.go
|
@ -3,17 +3,15 @@ package tg
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
Session *Session
|
||||
// To reach the bot abilities inside callbacks.
|
||||
Bot *Bot
|
||||
updates chan *Update
|
||||
// Is true if currently reading the Update.
|
||||
readingUpdate bool
|
||||
|
||||
curScreen, prevScreen *Screen
|
||||
widgetUpdates chan *Update
|
||||
CurScreen, PrevScreen *Screen
|
||||
}
|
||||
|
||||
// The type represents way to interact with user in
|
||||
|
@ -29,102 +27,46 @@ func (c *context) handleUpdateChan(updates chan *Update) {
|
|||
c.run(beh.Init, nil)
|
||||
}
|
||||
for u := range updates {
|
||||
var act Action
|
||||
screen := c.curScreen
|
||||
// The part is added to implement custom update handling.
|
||||
if u.Message != nil {
|
||||
if !session.Started {
|
||||
if u.Message.IsCommand() &&
|
||||
u.Message.Command() == "start" {
|
||||
// Special treatment for the "/start"
|
||||
// command.
|
||||
session.Started = true
|
||||
cmdName := CommandName("start")
|
||||
cmd, ok := beh.Commands[cmdName]
|
||||
if ok {
|
||||
act = cmd.Action
|
||||
} else {
|
||||
// Some usage.
|
||||
}
|
||||
} else {
|
||||
// Prestart handling.
|
||||
act = preStart
|
||||
}
|
||||
} else if u.Message.IsCommand() {
|
||||
// Command handling.
|
||||
cmdName := CommandName(u.Message.Command())
|
||||
if !session.started {
|
||||
if u.Message.IsCommand() &&
|
||||
u.Message.Command() == "start" {
|
||||
// Special treatment for the "/start"
|
||||
// command.
|
||||
session.started = true
|
||||
cmdName := CommandName("start")
|
||||
cmd, ok := beh.Commands[cmdName]
|
||||
if ok {
|
||||
act = cmd.Action
|
||||
if cmd.Action != nil {
|
||||
c.run(cmd.Action, u)
|
||||
}
|
||||
} else {
|
||||
// Some usage.
|
||||
}
|
||||
} else {
|
||||
// Simple messages handling.
|
||||
kbd := screen.Reply
|
||||
if kbd == nil {
|
||||
if c.readingUpdate {
|
||||
c.updates <- u
|
||||
}
|
||||
continue
|
||||
}
|
||||
btns := kbd.buttonMap()
|
||||
text := u.Message.Text
|
||||
btn, ok := btns[text]
|
||||
if !ok {
|
||||
if u.Message.Location != nil {
|
||||
for _, b := range btns {
|
||||
if b.SendLocation {
|
||||
btn = b
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
} else if c.readingUpdate {
|
||||
// Skipping the update sending it to
|
||||
// the reading goroutine.
|
||||
c.updates <- u
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
act = btn.Action
|
||||
}
|
||||
}
|
||||
} else if u.CallbackQuery != nil && session.Started {
|
||||
cb := tgbotapi.NewCallback(
|
||||
u.CallbackQuery.ID,
|
||||
u.CallbackQuery.Data,
|
||||
)
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
_, err := c.Bot.Api.Request(cb)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
kbd := screen.Inline
|
||||
if kbd == nil {
|
||||
if c.readingUpdate {
|
||||
c.updates <- u
|
||||
}
|
||||
continue
|
||||
// Prestart handling.
|
||||
c.run(preStart, u)
|
||||
}
|
||||
|
||||
btns := kbd.buttonMap()
|
||||
btn, ok := btns[data]
|
||||
if !ok && c.readingUpdate {
|
||||
c.updates <- u
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
c.Sendf("%q", btns)
|
||||
continue
|
||||
}
|
||||
act = btn.Action
|
||||
}
|
||||
if act != nil {
|
||||
c.run(act, u)
|
||||
continue
|
||||
}
|
||||
|
||||
if u.Message != nil && u.Message.IsCommand() {
|
||||
// Command handling.
|
||||
cmdName := CommandName(u.Message.Command())
|
||||
cmd, ok := beh.Commands[cmdName]
|
||||
if ok {
|
||||
if cmd.Action != nil {
|
||||
c.run(cmd.Action, u)
|
||||
}
|
||||
} else {
|
||||
// Some usage.
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// The standard thing - send messages to widgets.
|
||||
c.widgetUpdates <- u
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,29 +77,8 @@ func (c *context) run(a Action, u *Update) {
|
|||
})
|
||||
}
|
||||
|
||||
// Returns the next update ignoring current screen.
|
||||
func (c *context) ReadUpdate() (*Update, error) {
|
||||
c.readingUpdate = true
|
||||
u := <-c.updates
|
||||
c.readingUpdate = false
|
||||
if u == nil {
|
||||
return nil, NotAvailableErr
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Returns the next text message that the user sends.
|
||||
func (c *context) ReadTextMessage() (string, error) {
|
||||
u, err := c.ReadUpdate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Message == nil {
|
||||
return "", WrongUpdateType{}
|
||||
}
|
||||
|
||||
return u.Message.Text, nil
|
||||
func (c *context) Render(v Renderable) ([]*Message, error) {
|
||||
return c.Bot.Render(c.Session.Id, v)
|
||||
}
|
||||
|
||||
// Sends to the Sendable object.
|
||||
|
@ -217,19 +138,36 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
|
|||
// Stop the reading by sending the nil,
|
||||
// since we change the screen and
|
||||
// current goroutine needs to be stopped.
|
||||
if c.readingUpdate {
|
||||
c.updates <- nil
|
||||
}
|
||||
// if c.readingUpdate {
|
||||
// c.Updates <- nil
|
||||
// }
|
||||
|
||||
// Getting the screen and changing to
|
||||
// then executing its action.
|
||||
// then executing its widget.
|
||||
screen := c.Bot.behaviour.Screens[screenId]
|
||||
c.prevScreen = c.curScreen
|
||||
c.curScreen = screen
|
||||
c.Bot.Render(c.Session.Id, screen)
|
||||
if screen.Action != nil {
|
||||
c.run(screen.Action, c.Update)
|
||||
c.PrevScreen = c.CurScreen
|
||||
c.CurScreen = screen
|
||||
|
||||
// Making the new channel for the widget.
|
||||
if c.widgetUpdates != nil {
|
||||
close(c.widgetUpdates)
|
||||
}
|
||||
c.widgetUpdates = make(chan *Update)
|
||||
if screen.Widget != nil {
|
||||
// Running the widget if the screen has one.
|
||||
go screen.Widget.Serve(c, c.widgetUpdates)
|
||||
} else {
|
||||
// Skipping updates if there is no
|
||||
// widget to handle them.
|
||||
go func() {
|
||||
for _ = range c.widgetUpdates {}
|
||||
}()
|
||||
}
|
||||
|
||||
//c.Bot.Render(c.Session.Id, screen)
|
||||
//if screen.Action != nil {
|
||||
//c.run(screen.Action, c.Update)
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
97
tg/screen.go
97
tg/screen.go
|
@ -1,9 +1,5 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// Unique identifier for the screen.
|
||||
type ScreenId string
|
||||
|
||||
|
@ -11,100 +7,19 @@ type ScreenId string
|
|||
// Mostly what buttons to show.
|
||||
type Screen struct {
|
||||
Id ScreenId
|
||||
// The text to be displayed when the screen is
|
||||
// reached.
|
||||
Text string
|
||||
// The keyboard to be sent in the message part.
|
||||
Inline *InlineKeyboard
|
||||
// Keyboard to be displayed on the screen.
|
||||
Reply *ReplyKeyboard
|
||||
// Action called on the reaching the screen.
|
||||
Action *action
|
||||
// The widget to run when reaching the screen.
|
||||
Widget Widget
|
||||
|
||||
}
|
||||
|
||||
// Map structure for the screens.
|
||||
type ScreenMap map[ScreenId]*Screen
|
||||
|
||||
// Returns the new screen with specified Text and Keyboard.
|
||||
func NewScreen(id ScreenId) *Screen {
|
||||
// Returns the new screen with specified name and widget.
|
||||
func NewScreen(id ScreenId, widget Widget) *Screen {
|
||||
return &Screen{
|
||||
Id: id,
|
||||
Widget: widget,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the screen with specified text printing on appearing.
|
||||
func (s *Screen) WithText(text string) *Screen {
|
||||
s.Text = text
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Screen) WithInline(ikbd *InlineKeyboard) *Screen {
|
||||
s.Inline= ikbd
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Screen) WithReply(kbd *ReplyKeyboard) *Screen {
|
||||
s.Reply = kbd
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Screen) WithAction(a Action) *Screen {
|
||||
s.Action = newAction(a)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Screen) ActionFunc(a ActionFunc) *Screen {
|
||||
return s.WithAction(a)
|
||||
}
|
||||
|
||||
func (s *Screen) Render(
|
||||
sid SessionId, bot *Bot,
|
||||
) ([]*SendConfig, error) {
|
||||
cid := sid.ToApi()
|
||||
reply := s.Reply
|
||||
inline := s.Inline
|
||||
ret := []*SendConfig{}
|
||||
var txt string
|
||||
// Screen text and inline keyboard.
|
||||
if s.Text != "" {
|
||||
txt = s.Text
|
||||
} else if inline != nil {
|
||||
// Default to send the keyboard.
|
||||
txt = ">"
|
||||
}
|
||||
if txt != "" {
|
||||
msgConfig := tgbotapi.NewMessage(cid, txt)
|
||||
if inline != nil {
|
||||
msgConfig.ReplyMarkup = inline.ToApi()
|
||||
} else if reply != nil {
|
||||
msgConfig.ReplyMarkup = reply.ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
return ret, nil
|
||||
} else {
|
||||
msgConfig.ReplyMarkup = NewReply().
|
||||
WithRemove(true).
|
||||
ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
return ret, nil
|
||||
}
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
}
|
||||
|
||||
// Screen text and reply keyboard.
|
||||
if reply != nil {
|
||||
msgConfig := tgbotapi.NewMessage(cid, ">")
|
||||
msgConfig.ReplyMarkup = reply.ToApi()
|
||||
ret = append(ret, &SendConfig{
|
||||
Message: &msgConfig,
|
||||
})
|
||||
} else {
|
||||
// Removing keyboard if there is none.
|
||||
msgConfig := tgbotapi.NewMessage(cid, ">")
|
||||
msgConfig.ReplyMarkup = NewReply().
|
||||
WithRemove(true).
|
||||
ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type Session struct {
|
|||
Id SessionId
|
||||
// True if the session started.
|
||||
// (got the '/start' command.
|
||||
Started bool
|
||||
started bool
|
||||
// Custom value for each user.
|
||||
Value any
|
||||
}
|
||||
|
|
212
tg/widget.go
Normal file
212
tg/widget.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// Implementing the interface provides
|
||||
// ability to build your own widgets,
|
||||
// aka components.
|
||||
type Widget interface {
|
||||
// When the update channel is closed
|
||||
// widget MUST end its work.
|
||||
// Mostly made by looping over the
|
||||
// updates range.
|
||||
Serve(*Context, chan *Update)
|
||||
}
|
||||
|
||||
// The basic widget to provide keyboard functionality
|
||||
// without implementing much.
|
||||
type Page struct {
|
||||
Text string
|
||||
SubWidget Widget
|
||||
Inline *InlineKeyboard
|
||||
Reply *ReplyKeyboard
|
||||
Action Action
|
||||
}
|
||||
|
||||
// Return new page with the specified text.
|
||||
func NewPage(text string) *Page {
|
||||
ret := &Page{}
|
||||
ret.Text = text
|
||||
return ret
|
||||
}
|
||||
|
||||
// Set the inline keyboard.
|
||||
func (p *Page) WithInline(inline *InlineKeyboard) *Page {
|
||||
p.Inline = inline
|
||||
return p
|
||||
}
|
||||
|
||||
// Set the reply keyboard.
|
||||
func (p *Page) WithReply(reply *ReplyKeyboard) *Page {
|
||||
p.Reply = reply
|
||||
return p
|
||||
}
|
||||
|
||||
// Set the action to be run before serving.
|
||||
func (p *Page) WithAction(a Action) *Page {
|
||||
p.Action = a
|
||||
return p
|
||||
}
|
||||
|
||||
// Alias to with action to simpler define actions.
|
||||
func (p *Page) ActionFunc(fn ActionFunc) *Page {
|
||||
return p.WithAction(fn)
|
||||
}
|
||||
|
||||
// Set the sub widget that will get the skipped
|
||||
// updates.
|
||||
func (p *Page) WithSub(sub Widget) *Page {
|
||||
p.SubWidget = sub
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Page) Serve(
|
||||
c *Context, updates chan *Update,
|
||||
) {
|
||||
msgs, err := c.Render(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// The inline message is always returned
|
||||
// and the reply one is useless in our case.
|
||||
inlineMsg := msgs[0]
|
||||
|
||||
if p.Action != nil {
|
||||
c.run(p.Action, c.Update)
|
||||
}
|
||||
var subUpdates chan *Update
|
||||
if p.SubWidget != nil {
|
||||
subUpdates = make(chan *Update)
|
||||
go p.SubWidget.Serve(c, subUpdates)
|
||||
defer close(subUpdates)
|
||||
}
|
||||
for u := range updates {
|
||||
var act Action
|
||||
if u.Message != nil {
|
||||
text := u.Message.Text
|
||||
kbd := p.Reply
|
||||
if kbd == nil {
|
||||
if subUpdates != nil {
|
||||
subUpdates <- u
|
||||
}
|
||||
continue
|
||||
}
|
||||
btns := kbd.ButtonMap()
|
||||
btn, ok := btns[text]
|
||||
if !ok {
|
||||
if u.Message.Location != nil {
|
||||
for _, b := range btns {
|
||||
if b.SendLocation {
|
||||
btn = b
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
} else if subUpdates != nil {
|
||||
subUpdates <- u
|
||||
}
|
||||
}
|
||||
if btn != nil {
|
||||
act = btn.Action
|
||||
} else if kbd.Action != nil {
|
||||
act = kbd.Action
|
||||
}
|
||||
} else if u.CallbackQuery != nil {
|
||||
if u.CallbackQuery.Message.MessageID != inlineMsg.MessageID {
|
||||
if subUpdates != nil {
|
||||
subUpdates <- u
|
||||
}
|
||||
continue
|
||||
}
|
||||
cb := tgbotapi.NewCallback(
|
||||
u.CallbackQuery.ID,
|
||||
u.CallbackQuery.Data,
|
||||
)
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
_, err := c.Bot.Api.Request(cb)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
kbd := p.Inline
|
||||
if kbd == nil {
|
||||
if subUpdates != nil {
|
||||
subUpdates <- u
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
btns := kbd.ButtonMap()
|
||||
btn, ok := btns[data]
|
||||
if !ok {
|
||||
if subUpdates != nil {
|
||||
subUpdates <- u
|
||||
}
|
||||
continue
|
||||
}
|
||||
if btn != nil {
|
||||
act = btn.Action
|
||||
} else if kbd.Action != nil {
|
||||
act = kbd.Action
|
||||
}
|
||||
}
|
||||
if act != nil {
|
||||
c.run(act, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Page) Render(
|
||||
sid SessionId, bot *Bot,
|
||||
) ([]*SendConfig, error) {
|
||||
cid := sid.ToApi()
|
||||
reply := s.Reply
|
||||
inline := s.Inline
|
||||
ret := []*SendConfig{}
|
||||
var txt string
|
||||
// Screen text and inline keyboard.
|
||||
if s.Text != "" {
|
||||
txt = s.Text
|
||||
} else if inline != nil {
|
||||
// Default to send the keyboard.
|
||||
txt = ">"
|
||||
}
|
||||
if txt != "" {
|
||||
msgConfig := tgbotapi.NewMessage(cid, txt)
|
||||
if inline != nil {
|
||||
msgConfig.ReplyMarkup = inline.ToApi()
|
||||
} else if reply != nil {
|
||||
msgConfig.ReplyMarkup = reply.ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
return ret, nil
|
||||
} else {
|
||||
msgConfig.ReplyMarkup = NewReply().
|
||||
WithRemove(true).
|
||||
ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
return ret, nil
|
||||
}
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
}
|
||||
|
||||
// Screen text and reply keyboard.
|
||||
if reply != nil {
|
||||
msgConfig := tgbotapi.NewMessage(cid, ">")
|
||||
msgConfig.ReplyMarkup = reply.ToApi()
|
||||
ret = append(ret, &SendConfig{
|
||||
Message: &msgConfig,
|
||||
})
|
||||
} else {
|
||||
// Removing keyboard if there is none.
|
||||
msgConfig := tgbotapi.NewMessage(cid, ">")
|
||||
msgConfig.ReplyMarkup = NewReply().
|
||||
WithRemove(true).
|
||||
ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
Loading…
Reference in a new issue