Making the widgets more modulable. Needs to be finished.
This commit is contained in:
parent
5e1faf0c44
commit
57f85fdacc
15 changed files with 464 additions and 270 deletions
|
@ -4,29 +4,33 @@ tmp_dir = "tmp"
|
|||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./exe/test.exe"
|
||||
cmd = "go build -o ./exe/ ./cmd/..."
|
||||
bin = "exe/test.exe"
|
||||
cmd = "go build -o ./exe/ ./cmd/test"
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
|
||||
exclude_dir = ["app/volume", "assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_regex = ["_test.go", ".exe"]
|
||||
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll = true
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
app = "red"
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
|
@ -26,18 +26,21 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
|
|||
return ret
|
||||
}
|
||||
|
||||
func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) error {
|
||||
func (w *MutateMessageWidget) Serve(c *tg.Context, updates *tg.UpdateChan) {
|
||||
for _, arg := range c.Args {
|
||||
c.Sendf("%v", arg)
|
||||
}
|
||||
for u := range updates {
|
||||
if u.Message == nil {
|
||||
continue
|
||||
}
|
||||
for u := range updates.Chan() {
|
||||
text := u.Message.Text
|
||||
c.Sendf("%s", w.Mutate(text))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *MutateMessageWidget) Filter(u *tg.Update, _ tg.MessageMap) bool {
|
||||
if u.Message == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ExtractSessionData(c *tg.Context) *SessionData {
|
||||
|
@ -116,14 +119,14 @@ var beh = tg.NewBehaviour().
|
|||
c.Session.Data = &SessionData{}
|
||||
}).WithScreens(
|
||||
tg.NewScreen("start", tg.NewPage(
|
||||
"The bot started!",
|
||||
"",
|
||||
).WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton("GoT Github page").
|
||||
WithUrl("https://github.com/mojosa-software/got"),
|
||||
),
|
||||
).Widget(""),
|
||||
).WithReply(
|
||||
navKeyboard,
|
||||
navKeyboard.Widget("The bot started!"),
|
||||
),
|
||||
),
|
||||
tg.NewScreen("start/inc-dec", tg.NewPage(
|
||||
|
@ -132,7 +135,7 @@ var beh = tg.NewBehaviour().
|
|||
"by saving the counter for each of users "+
|
||||
"separately. ",
|
||||
).WithReply(
|
||||
incDecKeyboard,
|
||||
incDecKeyboard.Widget("Press the buttons to increment and decrement"),
|
||||
).ActionFunc(func(c *tg.Context) {
|
||||
// The function will be calleb before serving page.
|
||||
d := ExtractSessionData(c)
|
||||
|
@ -143,7 +146,7 @@ var beh = tg.NewBehaviour().
|
|||
tg.NewScreen("start/upper-case", tg.NewPage(
|
||||
"Type text and the bot will send you the upper case version to you",
|
||||
).WithReply(
|
||||
navToStartKeyboard,
|
||||
navToStartKeyboard.Widget(""),
|
||||
).WithSub(
|
||||
NewMutateMessageWidget(strings.ToUpper),
|
||||
),
|
||||
|
@ -152,7 +155,7 @@ var beh = tg.NewBehaviour().
|
|||
tg.NewScreen("start/lower-case", tg.NewPage(
|
||||
"Type text and the bot will send you the lower case version",
|
||||
).WithReply(
|
||||
navToStartKeyboard,
|
||||
navToStartKeyboard.Widget(""),
|
||||
).WithSub(
|
||||
NewMutateMessageWidget(strings.ToLower),
|
||||
),
|
||||
|
@ -161,7 +164,7 @@ var beh = tg.NewBehaviour().
|
|||
tg.NewScreen("start/send-location", tg.NewPage(
|
||||
"Send your location and I will tell where you are!",
|
||||
).WithReply(
|
||||
sendLocationKeyboard,
|
||||
sendLocationKeyboard.Widget(""),
|
||||
).WithInline(
|
||||
tg.NewInline().Row(
|
||||
tg.NewButton(
|
||||
|
@ -172,7 +175,7 @@ var beh = tg.NewBehaviour().
|
|||
d := ExtractSessionData(c)
|
||||
c.Sendf("Counter = %d", d.Counter)
|
||||
}),
|
||||
),
|
||||
).Widget("Press the button to display your counter"),
|
||||
),
|
||||
),
|
||||
).WithCommands(
|
||||
|
@ -189,9 +192,9 @@ var beh = tg.NewBehaviour().
|
|||
}),
|
||||
tg.NewCommand("read").
|
||||
Desc("reads a string and sends it back").
|
||||
WidgetFunc(func(c *tg.Context, updates chan *tg.Update) error {
|
||||
WidgetFunc(func(c *tg.Context, updates *tg.UpdateChan) {
|
||||
c.Sendf("Type text and I will send it back to you")
|
||||
for u := range updates {
|
||||
for u := range updates.Chan() {
|
||||
if u.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
@ -199,7 +202,6 @@ var beh = tg.NewBehaviour().
|
|||
break
|
||||
}
|
||||
c.Sendf("Done")
|
||||
return nil
|
||||
}),
|
||||
tg.NewCommand("image").
|
||||
Desc("sends a sample image").
|
||||
|
|
40
tg/bot.go
40
tg/bot.go
|
@ -50,9 +50,9 @@ func (bot *Bot) Debug(debug bool) *Bot {
|
|||
func (bot *Bot) Send(
|
||||
sid SessionId, v Sendable,
|
||||
) (*Message, error) {
|
||||
config, err := v.SendConfig(sid, bot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
config := v.SendConfig(sid, bot)
|
||||
if config.Error != nil {
|
||||
return nil, config.Error
|
||||
}
|
||||
|
||||
msg, err := bot.Api.Send(config.ToApi())
|
||||
|
@ -64,18 +64,22 @@ func (bot *Bot) Send(
|
|||
|
||||
func (bot *Bot) Render(
|
||||
sid SessionId, r Renderable,
|
||||
) ([]*Message, error) {
|
||||
configs, err := r.Render(sid, bot)
|
||||
if err != nil {
|
||||
return []*Message{}, err
|
||||
) (MessageMap, error) {
|
||||
configs := r.Render(sid, bot)
|
||||
if configs == nil {
|
||||
return nil, MapCollisionErr
|
||||
}
|
||||
messages := []*Message{}
|
||||
messages := make(MessageMap)
|
||||
for _, config := range configs {
|
||||
_, collision := messages[config.Name]
|
||||
if collision {
|
||||
return messages, MapCollisionErr
|
||||
}
|
||||
msg, err := bot.Api.Send(config.ToApi())
|
||||
if err != nil {
|
||||
return messages, err
|
||||
}
|
||||
messages = append(messages, &msg)
|
||||
messages[config.Name] = &msg
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
@ -204,7 +208,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) {
|
||||
chans := make(map[SessionId]chan *Update)
|
||||
chans := make(map[SessionId] *UpdateChan )
|
||||
var sid SessionId
|
||||
for u := range updates {
|
||||
sid = SessionId(u.FromChat().ID)
|
||||
|
@ -224,9 +228,12 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
|||
Bot: bot,
|
||||
Session: session,
|
||||
}
|
||||
chn := make(chan *Update)
|
||||
chn := NewUpdateChan()
|
||||
chans[sid] = chn
|
||||
go ctx.handleUpdateChan(chn)
|
||||
go (&Context{
|
||||
context: ctx,
|
||||
Update: u,
|
||||
}).Serve(chn)
|
||||
}
|
||||
} else if u.Message != nil {
|
||||
// Create session on any message
|
||||
|
@ -237,16 +244,19 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
|
|||
Bot: bot,
|
||||
Session: lsession,
|
||||
}
|
||||
chn := make(chan *Update)
|
||||
chn := NewUpdateChan()
|
||||
chans[sid] = chn
|
||||
go ctx.handleUpdateChan(chn)
|
||||
go (&Context{
|
||||
context: ctx,
|
||||
Update: u,
|
||||
}).Serve(chn)
|
||||
}
|
||||
|
||||
chn, ok := chans[sid]
|
||||
// The bot MUST get the "start" command.
|
||||
// It will do nothing otherwise.
|
||||
if ok {
|
||||
chn <- u
|
||||
chn.Send(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,7 +142,21 @@ func (w *CommandWidget) WithUsageFunc(fn ActionFunc) *CommandWidget {
|
|||
return w.WithUsage(fn)
|
||||
}
|
||||
|
||||
func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
|
||||
func (widget *Command) Filter(
|
||||
u *Update,
|
||||
msgs ...*Message,
|
||||
) bool {
|
||||
/*if u.Message == nil || !u.Message.IsCommand() {
|
||||
return false
|
||||
}*/
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (widget *CommandWidget) Serve(
|
||||
c *Context,
|
||||
updates *UpdateChan,
|
||||
) {
|
||||
commanders := make(map[CommandName] BotCommander)
|
||||
for k, v := range widget.Commands {
|
||||
commanders[k] = v
|
||||
|
@ -153,7 +167,7 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
|
|||
)
|
||||
|
||||
var cmdUpdates chan *Update
|
||||
for u := range updates {
|
||||
for u := range updates.Chan() {
|
||||
if c.ScreenId() == "" && u.Message != nil {
|
||||
// Skipping and executing the preinit action
|
||||
// while we have the empty screen.
|
||||
|
@ -178,13 +192,12 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
|
|||
if cmdUpdates != nil {
|
||||
close(cmdUpdates)
|
||||
}
|
||||
cmdUpdates = make(chan *Update)
|
||||
cmdUpdates := NewUpdateChan()
|
||||
go func() {
|
||||
cmd.Widget.Serve(
|
||||
&Context{context: c.context, Update: u},
|
||||
cmdUpdates,
|
||||
)
|
||||
close(cmdUpdates)
|
||||
cmdUpdates = nil
|
||||
}()
|
||||
}
|
||||
|
@ -199,5 +212,4 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
|
|||
c.Skip(u)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ var (
|
|||
NotAvailableErr = errors.New("the context is not available")
|
||||
EmptyKeyboardTextErr = errors.New("got empty text for a keyboard")
|
||||
ActionNotDefinedErr = errors.New("action was not defined")
|
||||
MapCollisionErr = errors.New("map collision occured")
|
||||
)
|
||||
|
||||
func (wut WrongUpdateType) Error() string {
|
||||
|
|
|
@ -74,7 +74,7 @@ func (f *File) SendData() string {
|
|||
}
|
||||
func (f *File) SendConfig(
|
||||
sid SessionId, bot *Bot,
|
||||
) (*SendConfig, error) {
|
||||
) (*SendConfig) {
|
||||
var config SendConfig
|
||||
cid := sid.ToApi()
|
||||
|
||||
|
@ -85,9 +85,9 @@ func (f *File) SendConfig(
|
|||
|
||||
config.Image = &photo
|
||||
default:
|
||||
return nil, UnknownFileTypeErr
|
||||
panic(UnknownFileTypeErr)
|
||||
}
|
||||
|
||||
|
||||
return &config, nil
|
||||
return &config
|
||||
}
|
||||
|
|
|
@ -83,13 +83,9 @@ func (c *groupContext) Sendf(
|
|||
format string,
|
||||
v ...any,
|
||||
) (*Message, error) {
|
||||
msg, err := c.Send(NewMessage(
|
||||
c.Session.Id, fmt.Sprintf(format, v...),
|
||||
return c.Send(NewMessage(
|
||||
fmt.Sprintf(format, v...),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// Sends into the chat specified values converted to strings.
|
||||
|
|
|
@ -10,6 +10,7 @@ type Keyboard struct {
|
|||
// defined action for the button.
|
||||
Action *action
|
||||
Rows []ButtonRow
|
||||
buttonMap ButtonMap
|
||||
}
|
||||
|
||||
// The type represents reply keyboards.
|
||||
|
@ -59,6 +60,14 @@ func (kbd *InlineKeyboard) ActionFunc(fn ActionFunc) *InlineKeyboard {
|
|||
return kbd.WithAction(fn)
|
||||
}
|
||||
|
||||
// Transform the keyboard to widget with the specified text.
|
||||
func (kbd *InlineKeyboard) Widget(text string) *InlineKeyboardWidget {
|
||||
ret := &InlineKeyboardWidget{}
|
||||
ret.InlineKeyboard = kbd
|
||||
ret.Text = text
|
||||
return ret
|
||||
}
|
||||
|
||||
// Adds a new button row to the current keyboard.
|
||||
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
|
||||
// For empty row. We do not need that.
|
||||
|
@ -80,6 +89,13 @@ func (kbd *ReplyKeyboard) ActionFunc(fn ActionFunc) *ReplyKeyboard {
|
|||
return kbd.WithAction(fn)
|
||||
}
|
||||
|
||||
func (kbd *ReplyKeyboard) Widget(text string) *ReplyKeyboardWidget {
|
||||
ret := &ReplyKeyboardWidget{}
|
||||
ret.ReplyKeyboard = kbd
|
||||
ret.Text = text
|
||||
return ret
|
||||
}
|
||||
|
||||
// Convert the Keyboard to the Telegram API type of reply keyboard.
|
||||
func (kbd *ReplyKeyboard) ToApi() any {
|
||||
// Shades everything.
|
||||
|
@ -133,12 +149,16 @@ func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
|
|||
|
||||
// Returns the map of buttons. Used to define the Action.
|
||||
func (kbd Keyboard) ButtonMap() ButtonMap {
|
||||
if kbd.buttonMap != nil {
|
||||
return kbd.buttonMap
|
||||
}
|
||||
ret := make(ButtonMap)
|
||||
for _, vi := range kbd.Rows {
|
||||
for _, vj := range vi {
|
||||
ret[vj.Key()] = vj
|
||||
}
|
||||
}
|
||||
kbd.buttonMap = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
@ -4,48 +4,23 @@ import (
|
|||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// Simple text message type.
|
||||
type MessageConfig struct {
|
||||
To SessionId
|
||||
ReplyTo MessageId
|
||||
Text string
|
||||
Inline *InlineKeyboard
|
||||
Reply *ReplyKeyboard
|
||||
}
|
||||
|
||||
func NewMessage(to SessionId, text string) *MessageConfig {
|
||||
// Return new message with the specified text.
|
||||
func NewMessage(text string) *MessageConfig {
|
||||
ret := &MessageConfig{}
|
||||
ret.To = to
|
||||
ret.Text = text
|
||||
return ret
|
||||
}
|
||||
|
||||
func (config *MessageConfig) WithInline(
|
||||
inline *InlineKeyboard,
|
||||
) *MessageConfig {
|
||||
config.Inline = inline
|
||||
return config
|
||||
}
|
||||
|
||||
func (config *MessageConfig) WithReply(
|
||||
reply *ReplyKeyboard,
|
||||
) *MessageConfig {
|
||||
config.Reply = reply
|
||||
return config
|
||||
}
|
||||
|
||||
func (config *MessageConfig) SendConfig(
|
||||
sid SessionId, bot *Bot,
|
||||
) (*SendConfig, error) {
|
||||
) (*SendConfig) {
|
||||
var ret SendConfig
|
||||
msg := tgbotapi.NewMessage(config.To.ToApi(), config.Text)
|
||||
if config.Inline != nil {
|
||||
msg.ReplyMarkup = config.Inline.ToApi()
|
||||
}
|
||||
// Reply shades the inline.
|
||||
if config.Reply != nil {
|
||||
msg.ReplyMarkup = config.Reply.ToApi()
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(sid.ToApi(), config.Text)
|
||||
ret.Message = &msg
|
||||
return &ret, nil
|
||||
return &ret
|
||||
}
|
||||
|
|
210
tg/page.go
210
tg/page.go
|
@ -1,7 +1,7 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// The basic widget to provide keyboard functionality
|
||||
|
@ -9,9 +9,9 @@ import (
|
|||
type Page struct {
|
||||
Text string
|
||||
SubWidget Widget
|
||||
Inline *InlineKeyboard
|
||||
Reply *ReplyKeyboard
|
||||
Inline *InlineKeyboardWidget
|
||||
Action Action
|
||||
Reply *ReplyKeyboardWidget
|
||||
}
|
||||
|
||||
// Return new page with the specified text.
|
||||
|
@ -22,13 +22,13 @@ func NewPage(text string) *Page {
|
|||
}
|
||||
|
||||
// Set the inline keyboard.
|
||||
func (p *Page) WithInline(inline *InlineKeyboard) *Page {
|
||||
func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page {
|
||||
p.Inline = inline
|
||||
return p
|
||||
}
|
||||
|
||||
// Set the reply keyboard.
|
||||
func (p *Page) WithReply(reply *ReplyKeyboard) *Page {
|
||||
func (p *Page) WithReply(reply *ReplyKeyboardWidget) *Page {
|
||||
p.Reply = reply
|
||||
return p
|
||||
}
|
||||
|
@ -51,150 +51,76 @@ func (p *Page) WithSub(sub Widget) *Page {
|
|||
return p
|
||||
}
|
||||
|
||||
func (p *Page) Serve(
|
||||
c *Context, updates chan *Update,
|
||||
) error {
|
||||
msgs, err := c.Render(p)
|
||||
if err != nil {
|
||||
return 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 {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
c.Run(act, u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Page) Render(
|
||||
func (p *Page) Render(
|
||||
sid SessionId, bot *Bot,
|
||||
) ([]*SendConfig, error) {
|
||||
cid := sid.ToApi()
|
||||
reply := s.Reply
|
||||
inline := s.Inline
|
||||
) ([]*SendConfig) {
|
||||
reply := p.Reply
|
||||
inline := p.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 p.Text != "" {
|
||||
cfg := NewMessage(p.Text).SendConfig(sid, bot).
|
||||
WithName("page/text")
|
||||
ret = append(ret, cfg)
|
||||
}
|
||||
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
|
||||
cfg := inline.SendConfig(sid, bot).
|
||||
WithName("page/inline")
|
||||
ret = append(ret, cfg)
|
||||
}
|
||||
if p.Reply != nil {
|
||||
cfg := reply.SendConfig(sid, bot).
|
||||
WithName("page/reply")
|
||||
ret = append(ret, cfg)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Page) Filter(
|
||||
u *Update, msgs MessageMap,
|
||||
) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Page) Serve(
|
||||
c *Context, updates *UpdateChan,
|
||||
) {
|
||||
msgs, _ := c.Render(p)
|
||||
inlineMsg := msgs["page/inline"]
|
||||
if p.Action != nil {
|
||||
c.Run(p.Action, c.Update)
|
||||
}
|
||||
|
||||
subUpdates := c.RunWidgetBg(p.SubWidget)
|
||||
defer subUpdates.Close()
|
||||
|
||||
inlineUpdates := c.RunWidgetBg(p.Inline)
|
||||
defer inlineUpdates.Close()
|
||||
|
||||
replyUpdates := c.RunWidgetBg(p.Reply)
|
||||
defer replyUpdates.Close()
|
||||
|
||||
subFilter, subFilterOk := p.SubWidget.(Filterer)
|
||||
for u := range updates.Chan() {
|
||||
switch {
|
||||
case !p.Inline.Filter(u, MessageMap{"": inlineMsg}) :
|
||||
inlineUpdates.Send(u)
|
||||
case !p.Reply.Filter(u, msgs) :
|
||||
replyUpdates.Send(u )
|
||||
case p.SubWidget != nil :
|
||||
if subFilterOk {
|
||||
if subFilter.Filter(u, msgs) {
|
||||
subUpdates.Send(u)
|
||||
}
|
||||
} else {
|
||||
msgConfig.ReplyMarkup = NewReply().
|
||||
WithRemove(true).
|
||||
ToApi()
|
||||
ret = append(ret, &SendConfig{Message: &msgConfig})
|
||||
return ret, nil
|
||||
subUpdates.Send(u)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ type context struct {
|
|||
Session *Session
|
||||
// To reach the bot abilities inside callbacks.
|
||||
Bot *Bot
|
||||
skippedUpdates chan *Update
|
||||
skippedUpdates *UpdateChan
|
||||
// Current screen ID.
|
||||
screenId, prevScreenId ScreenId
|
||||
}
|
||||
|
@ -20,14 +20,12 @@ type context struct {
|
|||
// handling functions. Is provided to Act() function always.
|
||||
|
||||
// Goroutie function to handle each user.
|
||||
func (c *context) handleUpdateChan(updates chan *Update) {
|
||||
func (c *Context) Serve(updates *UpdateChan) {
|
||||
beh := c.Bot.behaviour
|
||||
if beh.Init != nil {
|
||||
c.run(beh.Init, nil)
|
||||
c.Run(beh.Init, c.Update)
|
||||
}
|
||||
beh.Root.Serve(&Context{
|
||||
context: c,
|
||||
}, updates)
|
||||
beh.Root.Serve(c, updates)
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,14 +51,12 @@ func (c *Context) Run(a Action, u *Update) {
|
|||
// Skip the update sending it down to
|
||||
// the underlying widget.
|
||||
func (c *Context) Skip(u *Update) {
|
||||
if c.skippedUpdates != nil {
|
||||
c.skippedUpdates <- u
|
||||
}
|
||||
c.skippedUpdates.Send(u)
|
||||
}
|
||||
|
||||
// Renders the Renedrable object to the side of client
|
||||
// and returns the messages it sent.
|
||||
func (c *Context) Render(v Renderable) ([]*Message, error) {
|
||||
func (c *Context) Render(v Renderable) (MessageMap, error) {
|
||||
return c.Bot.Render(c.Session.Id, v)
|
||||
}
|
||||
|
||||
|
@ -71,13 +67,7 @@ func (c *Context) Send(v Sendable) (*Message, error) {
|
|||
|
||||
// Sends the formatted with fmt.Sprintf message to the user.
|
||||
func (c *Context) Sendf(format string, v ...any) (*Message, error) {
|
||||
msg, err := c.Send(NewMessage(
|
||||
c.Session.Id, fmt.Sprintf(format, v...),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
return c.Send(NewMessage(fmt.Sprintf(format, v...)))
|
||||
}
|
||||
|
||||
// Interface to interact with the user.
|
||||
|
@ -130,10 +120,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
|
|||
c.screenId = screenId
|
||||
|
||||
// Making the new channel for the widget.
|
||||
if c.skippedUpdates != nil {
|
||||
close(c.skippedUpdates)
|
||||
}
|
||||
c.skippedUpdates = make(chan *Update)
|
||||
c.skippedUpdates.Close()
|
||||
c.skippedUpdates = NewUpdateChan()
|
||||
if screen.Widget != nil {
|
||||
// Running the widget if the screen has one.
|
||||
go func() {
|
||||
|
@ -142,6 +130,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
|
|||
Update: c.Update,
|
||||
Args: args,
|
||||
}, c.skippedUpdates)
|
||||
|
||||
c.skippedUpdates.Close()
|
||||
}()
|
||||
} else {
|
||||
panic("no widget defined for the screen")
|
||||
|
@ -149,3 +139,7 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) ChangeToPrevScreen() {
|
||||
c.ChangeScreen(c.PrevScreenId())
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@ type Screen struct {
|
|||
Id ScreenId
|
||||
// The widget to run when reaching the screen.
|
||||
Widget Widget
|
||||
|
||||
// Needs implementation later.
|
||||
Dynamic DynamicWidget
|
||||
}
|
||||
|
||||
// Map structure for the screens.
|
||||
|
@ -27,8 +24,3 @@ func NewScreen(id ScreenId, widget Widget) *Screen {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Screen) WithDynamic(dynamic DynamicWidget) *Screen {
|
||||
s.Dynamic = dynamic
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
23
tg/send.go
23
tg/send.go
|
@ -10,25 +10,38 @@ type Image any
|
|||
// Implementing the interface lets the
|
||||
// value to be sent.
|
||||
type Sendable interface {
|
||||
SendConfig(SessionId, *Bot) (*SendConfig, error)
|
||||
SendConfig(SessionId, *Bot) *SendConfig
|
||||
}
|
||||
|
||||
type Renderable interface {
|
||||
Render(SessionId, *Bot) ([]*SendConfig, error)
|
||||
Render(SessionId, *Bot) ([]*SendConfig)
|
||||
}
|
||||
|
||||
type Errorer interface {
|
||||
Err() error
|
||||
}
|
||||
|
||||
// The type is used as an endpoint to send messages
|
||||
// via bot.Send .
|
||||
type SendConfig struct {
|
||||
// Simple message with text.
|
||||
// to add text use lower image
|
||||
// or see the ParseMode for tgbotapi .
|
||||
// The name will be used to store
|
||||
// the message in the map.
|
||||
Name string
|
||||
// Message with text and keyboard.
|
||||
Message *tgbotapi.MessageConfig
|
||||
|
||||
// The image to be sent.
|
||||
Image *tgbotapi.PhotoConfig
|
||||
Error error
|
||||
}
|
||||
|
||||
func (cfg *SendConfig) WithName(name string) *SendConfig {
|
||||
cfg.Name = name
|
||||
return cfg
|
||||
}
|
||||
|
||||
type MessageMap map[string] *Message
|
||||
|
||||
// Convert to the bot.Api.Send format.
|
||||
func (config *SendConfig) ToApi() tgbotapi.Chattable {
|
||||
if config.Message != nil {
|
||||
|
|
|
@ -14,9 +14,6 @@ func (si SessionId) ToApi() int64 {
|
|||
type Session struct {
|
||||
// Id of the chat of the user.
|
||||
Id SessionId
|
||||
// True if the session started.
|
||||
// (got the '/start' command.
|
||||
started bool
|
||||
// Custom value for each user.
|
||||
Data any
|
||||
}
|
||||
|
|
262
tg/widget.go
262
tg/widget.go
|
@ -1,7 +1,7 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// Implementing the interface provides
|
||||
|
@ -12,7 +12,80 @@ type Widget interface {
|
|||
// widget MUST end its work.
|
||||
// Mostly made by looping over the
|
||||
// updates range.
|
||||
Serve(*Context, chan *Update) error
|
||||
Serve(*Context, *UpdateChan)
|
||||
}
|
||||
|
||||
// Needs implementation.
|
||||
// Behaviour can be the root widget or something like
|
||||
// that.
|
||||
type RootWidget interface {
|
||||
Widget
|
||||
}
|
||||
|
||||
// Implementing the interface provides way
|
||||
// to know exactly what kind of updates
|
||||
// the widget needs.
|
||||
type Filterer interface {
|
||||
// Return true if should filter the update
|
||||
// and not send it inside the widget.
|
||||
Filter(*Update, MessageMap) bool
|
||||
}
|
||||
|
||||
// The type represents general update channel.
|
||||
type UpdateChan struct {
|
||||
chn chan *Update
|
||||
}
|
||||
|
||||
// Return new update channel.
|
||||
func NewUpdateChan() *UpdateChan {
|
||||
ret := &UpdateChan{}
|
||||
ret.chn = make(chan *Update)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (updates *UpdateChan) Chan() chan *Update {
|
||||
return updates.chn
|
||||
}
|
||||
|
||||
// Send an update to the channel.
|
||||
func (updates *UpdateChan) Send(u *Update) {
|
||||
if updates != nil && updates.chn == nil {
|
||||
return
|
||||
}
|
||||
updates.chn <- u
|
||||
}
|
||||
|
||||
// Read an update from the channel.
|
||||
func (updates *UpdateChan) Read() *Update {
|
||||
if updates == nil || updates.chn == nil {
|
||||
return nil
|
||||
}
|
||||
return <-updates.chn
|
||||
}
|
||||
|
||||
// Returns true if the channel is closed.
|
||||
func (updates *UpdateChan) Closed() bool {
|
||||
return updates.chn == nil
|
||||
}
|
||||
|
||||
// Close the channel. Used in defers.
|
||||
func (updates *UpdateChan) Close() {
|
||||
if updates == nil || updates.chn == nil {
|
||||
return
|
||||
}
|
||||
close(updates.chn)
|
||||
updates.chn = nil
|
||||
}
|
||||
|
||||
func (c *Context) RunWidgetBg(widget Widget) *UpdateChan {
|
||||
if widget == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
updates := NewUpdateChan()
|
||||
go widget.Serve(c, updates)
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Implementing the interface provides
|
||||
|
@ -22,9 +95,188 @@ type DynamicWidget interface {
|
|||
|
||||
// The function that implements the Widget
|
||||
// interface.
|
||||
type WidgetFunc func(*Context, chan *Update) error
|
||||
type WidgetFunc func(*Context, *UpdateChan)
|
||||
|
||||
func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error {
|
||||
return wf(c, updates)
|
||||
func (wf WidgetFunc) Serve(c *Context, updates *UpdateChan) {
|
||||
wf(c, updates)
|
||||
}
|
||||
|
||||
func (wf WidgetFunc) Filter(
|
||||
u *Update,
|
||||
msgs ...*Message,
|
||||
) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// The type implements message with an inline keyboard.
|
||||
type InlineKeyboardWidget struct {
|
||||
Text string
|
||||
*InlineKeyboard
|
||||
}
|
||||
|
||||
// The type implements dynamic inline keyboard widget.
|
||||
// Aka message with inline keyboard.
|
||||
func NewInlineKeyboardWidget(
|
||||
text string,
|
||||
inline *InlineKeyboard,
|
||||
) *InlineKeyboardWidget {
|
||||
ret := &InlineKeyboardWidget{}
|
||||
ret.Text = text
|
||||
ret.InlineKeyboard = inline
|
||||
return ret
|
||||
}
|
||||
func (widget *InlineKeyboardWidget) SendConfig(
|
||||
sid SessionId,
|
||||
bot *Bot,
|
||||
) (*SendConfig) {
|
||||
var text string
|
||||
if widget.Text != "" {
|
||||
text = widget.Text
|
||||
} else {
|
||||
text = ">"
|
||||
}
|
||||
|
||||
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
|
||||
msgConfig.ReplyMarkup = widget.ToApi()
|
||||
|
||||
ret := &SendConfig{}
|
||||
ret.Message = &msgConfig
|
||||
return ret
|
||||
}
|
||||
|
||||
func (widget *InlineKeyboardWidget) Serve(
|
||||
c *Context,
|
||||
updates *UpdateChan,
|
||||
) {
|
||||
for u := range updates.Chan() {
|
||||
var act Action
|
||||
if u.CallbackQuery == nil {
|
||||
continue
|
||||
}
|
||||
cb := tgbotapi.NewCallback(
|
||||
u.CallbackQuery.ID,
|
||||
u.CallbackQuery.Data,
|
||||
)
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
_, err := c.Bot.Api.Request(cb)
|
||||
if err != nil {
|
||||
//return err
|
||||
continue
|
||||
}
|
||||
|
||||
btns := widget.ButtonMap()
|
||||
btn, ok := btns[data]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if btn != nil {
|
||||
act = btn.Action
|
||||
} else if widget.Action != nil {
|
||||
act = widget.Action
|
||||
}
|
||||
c.Run(act, u)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *InlineKeyboardWidget) Filter(
|
||||
u *Update,
|
||||
msgs MessageMap,
|
||||
) bool {
|
||||
if widget == nil {
|
||||
return true
|
||||
}
|
||||
if u.CallbackQuery == nil || len(msgs) < 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
inlineMsg, inlineOk := msgs[""]
|
||||
if inlineOk {
|
||||
if u.CallbackQuery.Message.MessageID !=
|
||||
inlineMsg.MessageID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// The type implements dynamic reply keyboard widget.
|
||||
type ReplyKeyboardWidget struct {
|
||||
Text string
|
||||
*ReplyKeyboard
|
||||
}
|
||||
|
||||
// Returns new empty reply keyboard widget.
|
||||
func NewReplyKeyboardWidget(
|
||||
text string,
|
||||
reply *ReplyKeyboard,
|
||||
) *ReplyKeyboardWidget {
|
||||
ret := &ReplyKeyboardWidget{}
|
||||
ret.Text = text
|
||||
ret.ReplyKeyboard = reply
|
||||
return ret
|
||||
}
|
||||
|
||||
func (widget *ReplyKeyboardWidget) SendConfig(
|
||||
sid SessionId,
|
||||
bot *Bot,
|
||||
) (*SendConfig) {
|
||||
var text string
|
||||
if widget.Text != "" {
|
||||
text = widget.Text
|
||||
} else {
|
||||
text = ">"
|
||||
}
|
||||
|
||||
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
|
||||
msgConfig.ReplyMarkup = widget.ToApi()
|
||||
|
||||
ret := &SendConfig{}
|
||||
ret.Message = &msgConfig
|
||||
return ret
|
||||
}
|
||||
|
||||
func (widget *ReplyKeyboardWidget) Filter(
|
||||
u *Update,
|
||||
msgs MessageMap,
|
||||
) bool {
|
||||
if widget == nil {
|
||||
return true
|
||||
}
|
||||
if u.Message == nil {
|
||||
return true
|
||||
}
|
||||
_, ok := widget.ButtonMap()[u.Message.Text]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (widget *ReplyKeyboardWidget) Serve(
|
||||
c *Context,
|
||||
updates *UpdateChan,
|
||||
) {
|
||||
for u := range updates.Chan() {
|
||||
var btn *Button
|
||||
text := u.Message.Text
|
||||
btns := widget.ButtonMap()
|
||||
btn, ok := btns[text]
|
||||
if !ok {
|
||||
if u.Message.Location != nil {
|
||||
for _, b := range btns {
|
||||
if b.SendLocation {
|
||||
btn = b
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if btn != nil {
|
||||
c.Run(btn.Action, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue