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
|
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 (
|
var (
|
||||||
startScreenButton = tg.NewButton("🏠 To the start screen").
|
startScreenButton = tg.NewButton("🏠 To the start screen").
|
||||||
ScreenChange("start")
|
ScreenChange("start")
|
||||||
|
|
||||||
incDecKeyboard = tg.NewInline().Row(
|
incDecKeyboard = tg.NewReply().Row(
|
||||||
tg.NewButton("+").ActionFunc(func(c *tg.Context) {
|
tg.NewButton("+").ActionFunc(func(c *tg.Context) {
|
||||||
d := c.Session.Value.(*UserData)
|
d := c.Session.Value.(*UserData)
|
||||||
d.Counter++
|
d.Counter++
|
||||||
|
@ -38,12 +58,12 @@ var (
|
||||||
navKeyboard = tg.NewReply().
|
navKeyboard = tg.NewReply().
|
||||||
WithOneTime(true).
|
WithOneTime(true).
|
||||||
Row(
|
Row(
|
||||||
tg.NewButton("Inc/Dec").ScreenChange("inc/dec"),
|
tg.NewButton("Inc/Dec").ScreenChange("start/inc-dec"),
|
||||||
).Row(
|
).Row(
|
||||||
tg.NewButton("Upper case").ScreenChange("upper-case"),
|
tg.NewButton("Upper case").ScreenChange("start/upper-case"),
|
||||||
tg.NewButton("Lower case").ScreenChange("lower-case"),
|
tg.NewButton("Lower case").ScreenChange("start/lower-case"),
|
||||||
).Row(
|
).Row(
|
||||||
tg.NewButton("Send location").ScreenChange("send-location"),
|
tg.NewButton("Send location").ScreenChange("start/send-location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
sendLocationKeyboard = tg.NewReply().
|
sendLocationKeyboard = tg.NewReply().
|
||||||
|
@ -64,7 +84,7 @@ var (
|
||||||
l.Heading,
|
l.Heading,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, err = c.Sendf("Somehow wrong location was sent")
|
_, err = c.Sendf("Somehow location was not sent")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Sendf("%q", err)
|
c.Sendf("%q", err)
|
||||||
|
@ -89,62 +109,69 @@ var beh = tg.NewBehaviour().
|
||||||
WithPreStartFunc(func(c *tg.Context){
|
WithPreStartFunc(func(c *tg.Context){
|
||||||
c.Sendf("Please, use the /start command to start the bot")
|
c.Sendf("Please, use the /start command to start the bot")
|
||||||
}).WithScreens(
|
}).WithScreens(
|
||||||
tg.NewScreen("start").
|
tg.NewScreen("start", tg.NewPage(
|
||||||
WithText(
|
"The bot started!",
|
||||||
"The bot started!"+
|
).WithInline(
|
||||||
" The bot is supposed to provide basic"+
|
tg.NewInline().Row(
|
||||||
" understand of how the API works, so just"+
|
tg.NewButton("GoT Github page").
|
||||||
" horse around a bit to guess everything out"+
|
WithUrl("https://github.com/mojosa-software/got"),
|
||||||
" by yourself!",
|
),
|
||||||
).WithReply(navKeyboard).
|
).WithReply(
|
||||||
// The inline keyboard with link to GitHub page.
|
navKeyboard,
|
||||||
WithInline(
|
),
|
||||||
tg.NewInline().Row(
|
),
|
||||||
tg.NewButton("GoT Github page").
|
tg.NewScreen("start/inc-dec", tg.NewPage(
|
||||||
WithUrl("https://github.com/mojosa-software/got"),
|
"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").
|
tg.NewScreen("start/lower-case", tg.NewPage(
|
||||||
WithText(
|
"Type text and the bot will send you the lower case version",
|
||||||
"The screen shows how "+
|
).WithReply(
|
||||||
"user separated data works "+
|
navToStartKeyboard,
|
||||||
"by saving the counter for each of users "+
|
).WithSub(
|
||||||
"separately. ",
|
NewMutateMessageWidget(strings.ToLower),
|
||||||
).
|
),
|
||||||
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("upper-case").
|
tg.NewScreen("start/send-location", tg.NewPage(
|
||||||
WithText("Type text and the bot will send you the upper case version to you").
|
"Send your location and I will tell where you are!",
|
||||||
WithReply(navToStartKeyboard).
|
).WithReply(
|
||||||
ActionFunc(mutateMessage(strings.ToUpper)),
|
sendLocationKeyboard,
|
||||||
|
).WithInline(
|
||||||
tg.NewScreen("lower-case").
|
tg.NewInline().Row(
|
||||||
WithText("Type text and the bot will send you the lower case version").
|
tg.NewButton(
|
||||||
WithReply(navToStartKeyboard).
|
"Check",
|
||||||
ActionFunc(mutateMessage(strings.ToLower)),
|
).WithData(
|
||||||
|
"check",
|
||||||
tg.NewScreen("send-location").
|
).ActionFunc(func(c *tg.Context) {
|
||||||
WithText("Send your location and I will tell where you are!").
|
d := c.Session.Value.(*UserData)
|
||||||
WithReply(sendLocationKeyboard).
|
c.Sendf("Counter = %d", d.Counter)
|
||||||
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)
|
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).WithCommands(
|
).WithCommands(
|
||||||
tg.NewCommand("start").
|
tg.NewCommand("start").
|
||||||
Desc("start the bot").
|
Desc("start or restart the bot or move to the start screen").
|
||||||
ActionFunc(func(c *tg.Context){
|
ActionFunc(func(c *tg.Context){
|
||||||
c.ChangeScreen("start")
|
c.ChangeScreen("start")
|
||||||
}),
|
}),
|
||||||
|
@ -156,12 +183,12 @@ var beh = tg.NewBehaviour().
|
||||||
tg.NewCommand("read").
|
tg.NewCommand("read").
|
||||||
Desc("reads a string and sends it back").
|
Desc("reads a string and sends it back").
|
||||||
ActionFunc(func(c *tg.Context) {
|
ActionFunc(func(c *tg.Context) {
|
||||||
c.Sendf("Type some text:")
|
/*c.Sendf("Type some text:")
|
||||||
msg, err := c.ReadTextMessage()
|
msg, err := c.ReadTextMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Sendf("You typed %q", msg)
|
c.Sendf("You typed %q", msg)*/
|
||||||
}),
|
}),
|
||||||
tg.NewCommand("image").
|
tg.NewCommand("image").
|
||||||
Desc("sends a sample 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().
|
var gBeh = tg.NewGroupBehaviour().
|
||||||
InitFunc(func(c *tg.GC) {
|
InitFunc(func(c *tg.GC) {
|
||||||
}).
|
}).
|
||||||
|
|
|
@ -187,7 +187,6 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
||||||
ctx := &context{
|
ctx := &context{
|
||||||
Bot: bot,
|
Bot: bot,
|
||||||
Session: session,
|
Session: session,
|
||||||
updates: make(chan *Update),
|
|
||||||
}
|
}
|
||||||
chn := make(chan *Update)
|
chn := make(chan *Update)
|
||||||
chans[sid] = chn
|
chans[sid] = chn
|
||||||
|
@ -201,7 +200,6 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
||||||
ctx := &context{
|
ctx := &context{
|
||||||
Bot: bot,
|
Bot: bot,
|
||||||
Session: lsession,
|
Session: lsession,
|
||||||
updates: make(chan *Update),
|
|
||||||
}
|
}
|
||||||
chn := make(chan *Update)
|
chn := make(chan *Update)
|
||||||
chans[sid] = chn
|
chans[sid] = chn
|
||||||
|
|
|
@ -6,9 +6,13 @@ import (
|
||||||
|
|
||||||
// The general keyboard type used both in Reply and Inline.
|
// The general keyboard type used both in Reply and Inline.
|
||||||
type Keyboard struct {
|
type Keyboard struct {
|
||||||
|
// The action is called if there is no
|
||||||
|
// defined action for the button.
|
||||||
|
Action *action
|
||||||
Rows []ButtonRow
|
Rows []ButtonRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The type represents reply keyboards.
|
||||||
type ReplyKeyboard struct {
|
type ReplyKeyboard struct {
|
||||||
Keyboard
|
Keyboard
|
||||||
// If true will be removed after one press.
|
// If true will be removed after one press.
|
||||||
|
@ -17,16 +21,18 @@ type ReplyKeyboard struct {
|
||||||
Remove bool
|
Remove bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// The keyboard to be emdedded into the messages.
|
// The type represents keyboard to be emdedded into the messages.
|
||||||
type InlineKeyboard struct {
|
type InlineKeyboard struct {
|
||||||
Keyboard
|
Keyboard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns new empty inline keyboard.
|
||||||
func NewInline() *InlineKeyboard {
|
func NewInline() *InlineKeyboard {
|
||||||
ret := &InlineKeyboard{}
|
ret := &InlineKeyboard{}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns new empty reply keyboard.
|
||||||
func NewReply() *ReplyKeyboard {
|
func NewReply() *ReplyKeyboard {
|
||||||
ret := &ReplyKeyboard {}
|
ret := &ReplyKeyboard {}
|
||||||
return ret
|
return ret
|
||||||
|
@ -41,6 +47,18 @@ func (kbd *InlineKeyboard) Row(btns ...*Button) *InlineKeyboard {
|
||||||
kbd.Rows = append(kbd.Rows, btns)
|
kbd.Rows = append(kbd.Rows, btns)
|
||||||
return kbd
|
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.
|
// Adds a new button row to the current keyboard.
|
||||||
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
||||||
// For empty row. We do not need that.
|
// For empty row. We do not need that.
|
||||||
|
@ -51,6 +69,17 @@ func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
||||||
return kbd
|
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.
|
// Convert the Keyboard to the Telegram API type of reply keyboard.
|
||||||
func (kbd *ReplyKeyboard) ToApi() any {
|
func (kbd *ReplyKeyboard) ToApi() any {
|
||||||
// Shades everything.
|
// Shades everything.
|
||||||
|
@ -74,6 +103,7 @@ func (kbd *ReplyKeyboard) ToApi() any {
|
||||||
return tgbotapi.NewReplyKeyboard(rows...)
|
return tgbotapi.NewReplyKeyboard(rows...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert the inline keyboard to markup for the tgbotapi.
|
||||||
func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
|
func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
|
||||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||||
for _, row := range kbd.Rows {
|
for _, row := range kbd.Rows {
|
||||||
|
@ -87,18 +117,22 @@ func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
|
||||||
return tgbotapi.NewInlineKeyboardMarkup(rows...)
|
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 {
|
func (kbd *ReplyKeyboard) WithRemove(remove bool) *ReplyKeyboard {
|
||||||
kbd.Remove = remove
|
kbd.Remove = remove
|
||||||
return kbd
|
return kbd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set if the keyboard should be hidden after
|
||||||
|
// one of buttons is pressede.
|
||||||
func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
|
func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
|
||||||
kbd.OneTime = oneTime
|
kbd.OneTime = oneTime
|
||||||
return kbd
|
return kbd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the map of buttons. Used to define the Action.
|
// Returns the map of buttons. Used to define the Action.
|
||||||
func (kbd Keyboard) buttonMap() ButtonMap {
|
func (kbd Keyboard) ButtonMap() ButtonMap {
|
||||||
ret := make(ButtonMap)
|
ret := make(ButtonMap)
|
||||||
for _, vi := range kbd.Rows {
|
for _, vi := range kbd.Rows {
|
||||||
for _, vj := range vi {
|
for _, vj := range vi {
|
||||||
|
@ -108,3 +142,4 @@ func (kbd Keyboard) buttonMap() ButtonMap {
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
184
tg/private.go
184
tg/private.go
|
@ -3,17 +3,15 @@ package tg
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"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 {
|
type context struct {
|
||||||
Session *Session
|
Session *Session
|
||||||
|
// To reach the bot abilities inside callbacks.
|
||||||
Bot *Bot
|
Bot *Bot
|
||||||
updates chan *Update
|
widgetUpdates chan *Update
|
||||||
// Is true if currently reading the Update.
|
CurScreen, PrevScreen *Screen
|
||||||
readingUpdate bool
|
|
||||||
|
|
||||||
curScreen, prevScreen *Screen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type represents way to interact with user in
|
// 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)
|
c.run(beh.Init, nil)
|
||||||
}
|
}
|
||||||
for u := range updates {
|
for u := range updates {
|
||||||
var act Action
|
|
||||||
screen := c.curScreen
|
|
||||||
// The part is added to implement custom update handling.
|
// The part is added to implement custom update handling.
|
||||||
if u.Message != nil {
|
if !session.started {
|
||||||
if !session.Started {
|
if u.Message.IsCommand() &&
|
||||||
if u.Message.IsCommand() &&
|
u.Message.Command() == "start" {
|
||||||
u.Message.Command() == "start" {
|
// Special treatment for the "/start"
|
||||||
// Special treatment for the "/start"
|
// command.
|
||||||
// command.
|
session.started = true
|
||||||
session.Started = true
|
cmdName := CommandName("start")
|
||||||
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())
|
|
||||||
cmd, ok := beh.Commands[cmdName]
|
cmd, ok := beh.Commands[cmdName]
|
||||||
if ok {
|
if ok {
|
||||||
act = cmd.Action
|
if cmd.Action != nil {
|
||||||
|
c.run(cmd.Action, u)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Some usage.
|
// Some usage.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Simple messages handling.
|
// Prestart handling.
|
||||||
kbd := screen.Reply
|
c.run(preStart, u)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btns := kbd.buttonMap()
|
continue
|
||||||
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)
|
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) Render(v Renderable) ([]*Message, error) {
|
||||||
func (c *context) ReadUpdate() (*Update, error) {
|
return c.Bot.Render(c.Session.Id, v)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends to the Sendable object.
|
// Sends to the Sendable object.
|
||||||
|
@ -217,19 +138,36 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
|
||||||
// Stop the reading by sending the nil,
|
// Stop the reading by sending the nil,
|
||||||
// since we change the screen and
|
// since we change the screen and
|
||||||
// current goroutine needs to be stopped.
|
// current goroutine needs to be stopped.
|
||||||
if c.readingUpdate {
|
// if c.readingUpdate {
|
||||||
c.updates <- nil
|
// c.Updates <- nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Getting the screen and changing to
|
// Getting the screen and changing to
|
||||||
// then executing its action.
|
// then executing its widget.
|
||||||
screen := c.Bot.behaviour.Screens[screenId]
|
screen := c.Bot.behaviour.Screens[screenId]
|
||||||
c.prevScreen = c.curScreen
|
c.PrevScreen = c.CurScreen
|
||||||
c.curScreen = screen
|
c.CurScreen = screen
|
||||||
c.Bot.Render(c.Session.Id, screen)
|
|
||||||
if screen.Action != nil {
|
// Making the new channel for the widget.
|
||||||
c.run(screen.Action, c.Update)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
97
tg/screen.go
97
tg/screen.go
|
@ -1,9 +1,5 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
import (
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unique identifier for the screen.
|
// Unique identifier for the screen.
|
||||||
type ScreenId string
|
type ScreenId string
|
||||||
|
|
||||||
|
@ -11,100 +7,19 @@ type ScreenId string
|
||||||
// Mostly what buttons to show.
|
// Mostly what buttons to show.
|
||||||
type Screen struct {
|
type Screen struct {
|
||||||
Id ScreenId
|
Id ScreenId
|
||||||
// The text to be displayed when the screen is
|
// The widget to run when reaching the screen.
|
||||||
// reached.
|
Widget Widget
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map structure for the screens.
|
// Map structure for the screens.
|
||||||
type ScreenMap map[ScreenId]*Screen
|
type ScreenMap map[ScreenId]*Screen
|
||||||
|
|
||||||
// Returns the new screen with specified Text and Keyboard.
|
// Returns the new screen with specified name and widget.
|
||||||
func NewScreen(id ScreenId) *Screen {
|
func NewScreen(id ScreenId, widget Widget) *Screen {
|
||||||
return &Screen{
|
return &Screen{
|
||||||
Id: id,
|
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
|
Id SessionId
|
||||||
// True if the session started.
|
// True if the session started.
|
||||||
// (got the '/start' command.
|
// (got the '/start' command.
|
||||||
Started bool
|
started bool
|
||||||
// Custom value for each user.
|
// Custom value for each user.
|
||||||
Value any
|
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