Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
045a15cbea | |||
7f23e5a13e | |||
7a9a8be397 | |||
2d5acbd112 | |||
8b76e52e6f | |||
66c70d5b31 | |||
94ccb0b724 | |||
bae779af8a | |||
1471d7cdae | |||
c576e891b8 | |||
e119b75640 |
35 changed files with 1146 additions and 944 deletions
46
beh.go
46
beh.go
|
@ -1,65 +1,46 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
// The package implements
|
|
||||||
// behaviour for the Telegram bots.
|
|
||||||
|
|
||||||
// The type describes behaviour for the bot in personal chats.
|
// The type describes behaviour for the bot in personal chats.
|
||||||
type Behaviour struct {
|
type Behaviour struct {
|
||||||
Root Component
|
Root Component
|
||||||
Init Action
|
Init Action
|
||||||
Screens ScreenMap
|
//Screens ScreenMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns new empty behaviour.
|
// Returns new empty behaviour.
|
||||||
func NewBehaviour() *Behaviour {
|
func NewBehaviour() *Behaviour {
|
||||||
return &Behaviour{
|
return &Behaviour{
|
||||||
Screens: make(ScreenMap),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Action will be called on session creation,
|
// The Action will be called on session creation,
|
||||||
// not when starting or restarting the bot with the Start Action.
|
// not when starting or restarting the bot with the Start Action.
|
||||||
func (b *Behaviour) WithInit(a Action) *Behaviour {
|
func (b *Behaviour) SetInit(a Action) *Behaviour {
|
||||||
b.Init = a
|
b.Init = a
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias to WithInit to simplify behaviour definitions.
|
/*func (b *Behaviour) SetScreens(screens ScreenMap) *Behaviour {
|
||||||
func (b *Behaviour) WithInitFunc(
|
b.Screens = screens
|
||||||
fn ActionFunc,
|
return b
|
||||||
) *Behaviour {
|
|
||||||
return b.WithInit(fn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Behaviour) WithRootNode(node *RootNode) *Behaviour {
|
// Sets the root node of the Behaviour.
|
||||||
|
// Mostly used for commands and such stuff.
|
||||||
|
func (b *Behaviour) SetRootNode(node *RootNode) *Behaviour {
|
||||||
b.Screens = node.ScreenMap()
|
b.Screens = node.ScreenMap()
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// The function sets screens.
|
*/
|
||||||
/*func (b *Behaviour) WithScreens(
|
|
||||||
screens ...*Screen,
|
|
||||||
) *Behaviour {
|
|
||||||
for _, screen := range screens {
|
|
||||||
if screen.Id == "" {
|
|
||||||
panic("empty screen ID")
|
|
||||||
}
|
|
||||||
_, ok := b.Screens[screen.Id]
|
|
||||||
if ok {
|
|
||||||
panic("duplicate keyboard IDs")
|
|
||||||
}
|
|
||||||
b.Screens[screen.Id] = screen
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// The function sets as the standard root widget CommandWidget
|
// The function sets as the standard root widget CommandWidget
|
||||||
// and its commands..
|
// and its commands..
|
||||||
func (b *Behaviour) WithRoot(root Component) *Behaviour {
|
func (b *Behaviour) SetRootWidget(root Component) *Behaviour {
|
||||||
b.Root = root
|
b.Root = root
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// Check whether the screen exists in the behaviour.
|
// Check whether the screen exists in the behaviour.
|
||||||
func (beh *Behaviour) PathExist(pth Path) bool {
|
func (beh *Behaviour) PathExist(pth Path) bool {
|
||||||
_, ok := beh.Screens[pth]
|
_, ok := beh.Screens[pth]
|
||||||
|
@ -68,7 +49,6 @@ func (beh *Behaviour) PathExist(pth Path) bool {
|
||||||
|
|
||||||
// Returns the screen by it's ID.
|
// Returns the screen by it's ID.
|
||||||
func (beh *Behaviour) GetScreen(pth Path) *Screen {
|
func (beh *Behaviour) GetScreen(pth Path) *Screen {
|
||||||
pth = pth.Clean()
|
|
||||||
if !beh.PathExist(pth) {
|
if !beh.PathExist(pth) {
|
||||||
panic(ScreenNotExistErr)
|
panic(ScreenNotExistErr)
|
||||||
}
|
}
|
||||||
|
@ -76,4 +56,4 @@ func (beh *Behaviour) GetScreen(pth Path) *Screen {
|
||||||
screen := beh.Screens[pth]
|
screen := beh.Screens[pth]
|
||||||
return screen
|
return screen
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
126
bot.go
126
bot.go
|
@ -38,12 +38,12 @@ func NewBot(token string) (*Bot, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot) Debug(debug bool) *Bot {
|
func (bot *Bot) SetDebug(debug bool) *Bot {
|
||||||
bot.api.Debug = debug
|
bot.api.Debug = debug
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot) Api() *tgbotapi.BotAPI {
|
func (bot *Bot) API() *tgbotapi.BotAPI {
|
||||||
return bot.api
|
return bot.api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,36 +53,35 @@ func (bot *Bot) Me() User {
|
||||||
|
|
||||||
// Send the Renderable to the specified session client side.
|
// Send the Renderable to the specified session client side.
|
||||||
// Can be used for both group and private sessions because
|
// Can be used for both group and private sessions because
|
||||||
// SessionId represents both for chat IDs.
|
// SessionID represents both for chat IDs.
|
||||||
func (bot *Bot) Send(
|
func (bot *Bot) Send(
|
||||||
sid SessionId, v Sendable,
|
sid SessionID, v Sendable,
|
||||||
) (Message, error) {
|
) (*Message, error) {
|
||||||
config := v.SendConfig(sid, bot)
|
config := v.SendConfig(sid, bot)
|
||||||
if config.Error != nil {
|
if config.Error != nil {
|
||||||
return Message{}, config.Error
|
return nil, config.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := bot.api.Send(config.ToApi())
|
msg, err := bot.api.Send(config.ToAPI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Message{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
v.SetMessage(msg)
|
v.SetMessage(&msg)
|
||||||
return msg, nil
|
return &msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot) Sendf(
|
func (bot *Bot) Sendf(
|
||||||
sid SessionId, format string, v ...any,
|
sid SessionID, format string, v ...any,
|
||||||
) (Message, error){
|
) (*Message, error){
|
||||||
msg := Messagef(format, v...)
|
|
||||||
return bot.Send(
|
return bot.Send(
|
||||||
sid,
|
sid,
|
||||||
&msg,
|
Messagef(format, v...),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to the session specified its ID raw chattable from the tgbotapi.
|
// Send to the session specified its ID raw chattable from the tgbotapi.
|
||||||
func (bot *Bot) SendRaw(
|
func (bot *Bot) SendRaw(
|
||||||
sid SessionId, v tgbotapi.Chattable,
|
sid SessionID, v tgbotapi.Chattable,
|
||||||
) (*Message, error) {
|
) (*Message, error) {
|
||||||
msg, err := bot.api.Send(v)
|
msg, err := bot.api.Send(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -93,20 +92,29 @@ func (bot *Bot) SendRaw(
|
||||||
|
|
||||||
// Get session by its ID. Can be used for any scope
|
// Get session by its ID. Can be used for any scope
|
||||||
// including private, group and channel.
|
// including private, group and channel.
|
||||||
func (bot *Bot) GetSession(
|
func (bot *Bot) GotSession(
|
||||||
sid SessionId,
|
sid SessionID,
|
||||||
) (*Session, bool) {
|
) (*Session, bool) {
|
||||||
session, ok := bot.sessions[sid]
|
session, ok := bot.sessions[sid]
|
||||||
return session, ok
|
return session, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) WithBehaviour(beh *Behaviour) *Bot {
|
func (bot *Bot) SetData(v any) *Bot {
|
||||||
|
bot.data = v
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) Data() any {
|
||||||
|
return bot.data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) SetBehaviour(beh *Behaviour) *Bot {
|
||||||
b.behaviour = beh
|
b.behaviour = beh
|
||||||
b.sessions = make(SessionMap)
|
b.sessions = make(SessionMap)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) WithSessions(sessions SessionMap) *Bot {
|
func (b *Bot) SetSessions(sessions SessionMap) *Bot {
|
||||||
b.sessions = sessions
|
b.sessions = sessions
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
@ -140,7 +148,7 @@ func (bot *Bot) SetCommands(
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sort.Strings(names)
|
||||||
|
|
||||||
cmds := []*Command{}
|
cmds := []Command{}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
cmds = append(
|
cmds = append(
|
||||||
cmds,
|
cmds,
|
||||||
|
@ -150,7 +158,7 @@ func (bot *Bot) SetCommands(
|
||||||
|
|
||||||
botCmds := []tgbotapi.BotCommand{}
|
botCmds := []tgbotapi.BotCommand{}
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
botCmds = append(botCmds, cmd.ToApi())
|
botCmds = append(botCmds, cmd.ToAPI())
|
||||||
}
|
}
|
||||||
|
|
||||||
//tgbotapi.NewBotCommandScopeAllPrivateChats(),
|
//tgbotapi.NewBotCommandScopeAllPrivateChats(),
|
||||||
|
@ -202,7 +210,7 @@ func (bot *Bot) Run() error {
|
||||||
go bot.handleGroup(chn)
|
go bot.handleGroup(chn)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
me, _ := bot.Api.GetMe()
|
me, _ := bot.API().GetMe()
|
||||||
bot.me = me
|
bot.me = me
|
||||||
for up := range updates {
|
for up := range updates {
|
||||||
u := Update{
|
u := Update{
|
||||||
|
@ -216,6 +224,7 @@ func (bot *Bot) Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
chn, ok := handles[fromChat.Type]
|
chn, ok := handles[fromChat.Type]
|
||||||
|
// Skipping non existent scope.
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -229,67 +238,32 @@ func (bot *Bot) Run() error {
|
||||||
// The function handles updates supposed for the private
|
// The function handles updates supposed for the private
|
||||||
// chat with the bot.
|
// chat with the bot.
|
||||||
func (bot *Bot) handlePrivate(updates chan Update) {
|
func (bot *Bot) handlePrivate(updates chan Update) {
|
||||||
var sid SessionId
|
var sid SessionID
|
||||||
for u := range updates {
|
for u := range updates {
|
||||||
sid = SessionId(u.FromChat().ID)
|
sid = SessionID(u.FromChat().ID)
|
||||||
ctx, ctxOk := bot.contexts[sid]
|
session, sessionOk := bot.sessions[sid]
|
||||||
if u.Message != nil && !ctxOk {
|
if u.Message != nil && !sessionOk {
|
||||||
|
// Creating session if we have none
|
||||||
|
// but only on text messages.
|
||||||
|
session = bot.sessions.Add(bot, sid, PrivateSessionScope)
|
||||||
|
|
||||||
session, sessionOk := bot.sessions[sid]
|
// Creating the root context
|
||||||
if !sessionOk {
|
// that takes updates directly from
|
||||||
// Creating session if we have none.
|
// the session.
|
||||||
session = bot.sessions.Add(sid, PrivateSessionScope)
|
rootContext := Context{
|
||||||
}
|
|
||||||
session = bot.sessions[sid]
|
|
||||||
|
|
||||||
// Create context on any message
|
|
||||||
// if we have no one.
|
|
||||||
ctx = &context{
|
|
||||||
Bot: bot,
|
|
||||||
Session: session,
|
|
||||||
updates: NewUpdateChan(),
|
|
||||||
}
|
|
||||||
if !ctxOk {
|
|
||||||
bot.contexts[sid] = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
go Context{
|
|
||||||
session: session,
|
session: session,
|
||||||
bot: bot,
|
update: u,
|
||||||
Update: u,
|
input: session.updates,
|
||||||
input: ctx.updates,
|
}
|
||||||
}.serve()
|
go rootContext.serve()
|
||||||
ctx.session.updates.Send(u)
|
rootContext.input.Send(u)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctxOk {
|
if sessionOk {
|
||||||
ctx.updates.Send(u)
|
session.updates.Send(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
func (bot *Bot) handleGroup(updates chan *Update) {
|
|
||||||
var sid SessionId
|
|
||||||
chans := make(map[SessionId]chan *Update)
|
|
||||||
for u := range updates {
|
|
||||||
sid = SessionId(u.FromChat().ID)
|
|
||||||
// If no session add new.
|
|
||||||
if _, ok := bot.groupSessions[sid]; !ok {
|
|
||||||
bot.groupSessions.Add(sid)
|
|
||||||
session := bot.groupSessions[sid]
|
|
||||||
ctx := &groupContext{
|
|
||||||
Bot: bot,
|
|
||||||
Session: session,
|
|
||||||
updates: make(chan *Update),
|
|
||||||
}
|
|
||||||
chn := make(chan *Update)
|
|
||||||
chans[sid] = chn
|
|
||||||
go ctx.handleUpdateChan(chn)
|
|
||||||
}
|
|
||||||
|
|
||||||
chn := chans[sid]
|
|
||||||
chn <- u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
3
btest
Executable file
3
btest
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
go build -o ./exe/test ./cmd/test
|
54
button.go
54
button.go
|
@ -11,34 +11,40 @@ import (
|
||||||
type Button struct {
|
type Button struct {
|
||||||
Text string
|
Text string
|
||||||
Data string
|
Data string
|
||||||
Url string
|
URL string
|
||||||
SendLocation bool
|
SendLocation bool
|
||||||
Action Action
|
Action Action
|
||||||
|
// Used to skip buttons in generating by functions.
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonMap map[string]Button
|
type ButtonMap map[string]Button
|
||||||
|
|
||||||
// Returns the only location button in the map.
|
// Returns the only location button in the map and if it is there at all.
|
||||||
func (btnMap ButtonMap) LocationButton() *Button {
|
// The location map must be the ONLY one.
|
||||||
|
|
||||||
|
func (btnMap ButtonMap) LocationButton() (Button, bool) {
|
||||||
for _, btn := range btnMap {
|
for _, btn := range btnMap {
|
||||||
if btn.SendLocation {
|
if btn.SendLocation {
|
||||||
return btn
|
return btn, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return Button{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents the reply button row.
|
// Represents the reply button row.
|
||||||
type ButtonRow []*Button
|
type ButtonRow []Button
|
||||||
|
|
||||||
// Returns new button with the specified text and no action.
|
// Returns new button with the specified text and no action.
|
||||||
func Buttonf(format string, v ...any) Button {
|
func Buttonf(format string, v ...any) Button {
|
||||||
return &Button{
|
return Button{
|
||||||
Text: fmt.Sprintf(format, v...),
|
Text: fmt.Sprintf(format, v...),
|
||||||
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Randomize buttons data to make the key unique.
|
// Randomize buttons data to make the key unique.
|
||||||
|
// No guaranties about collisions though.
|
||||||
func (btn Button) Rand() Button {
|
func (btn Button) Rand() Button {
|
||||||
rData := make([]byte, 8)
|
rData := make([]byte, 8)
|
||||||
rand.Read(rData)
|
rand.Read(rData)
|
||||||
|
@ -49,8 +55,8 @@ func (btn Button) Rand() Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the URL for the button. Only for inline buttons.
|
// Set the URL for the button. Only for inline buttons.
|
||||||
func (btn Button) WithUrl(format string, v ...any) Button {
|
func (btn Button) WithURL(format string, v ...any) Button {
|
||||||
btn.Url = fmt.Sprintf(format, v...)
|
btn.URL = fmt.Sprintf(format, v...)
|
||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,10 +78,16 @@ func (btn Button) WithSendLocation(ok bool) Button {
|
||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (btn Button) Go(pth Path, args ...any) Button {
|
func (btn Button) Go(pth Widget) Button {
|
||||||
return btn.WithAction(ScreenGo{
|
return btn.WithAction(WidgetGo{
|
||||||
Path: pth,
|
Path: pth,
|
||||||
Args: args,
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (btn Button) GoWithArg(pth Widget, arg any) Button {
|
||||||
|
return btn.WithAction(WidgetGo{
|
||||||
|
Path: pth,
|
||||||
|
Arg: arg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +101,17 @@ func (btn Button) ToTelegram() apix.KeyboardButton {
|
||||||
|
|
||||||
func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
|
func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
|
||||||
if btn.Data != "" {
|
if btn.Data != "" {
|
||||||
return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data)
|
return apix.NewInlineKeyboardButtonData(
|
||||||
|
btn.Text,
|
||||||
|
btn.Data,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if btn.Url != "" {
|
if btn.URL != "" {
|
||||||
return apix.NewInlineKeyboardButtonURL(btn.Text, btn.Url)
|
return apix.NewInlineKeyboardButtonURL(
|
||||||
|
btn.Text,
|
||||||
|
btn.URL,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no match then return the data one with data the same as the text.
|
// If no match then return the data one with data the same as the text.
|
||||||
|
@ -102,9 +120,6 @@ func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
|
||||||
|
|
||||||
// Return the key of the button to identify it by messages and callbacks.
|
// Return the key of the button to identify it by messages and callbacks.
|
||||||
func (btn Button) Key() string {
|
func (btn Button) Key() string {
|
||||||
if btn == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if btn.Data != "" {
|
if btn.Data != "" {
|
||||||
return btn.Data
|
return btn.Data
|
||||||
}
|
}
|
||||||
|
@ -113,6 +128,7 @@ func (btn Button) Key() string {
|
||||||
return btn.Text
|
return btn.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewButtonRow(btns ...*Button) ButtonRow {
|
func NewButtonRow(btns ...Button) ButtonRow {
|
||||||
return btns
|
return btns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
86
cmd/test/cmd.go
Normal file
86
cmd/test/cmd.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var UsageAction = tg.Func(func(c tg.Context) {
|
||||||
|
c.Sendf(
|
||||||
|
"There is no such command %q",
|
||||||
|
c.CallbackUpdate().Message.Command(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
var PreStartAction = tg.Func(func(c tg.Context) {
|
||||||
|
c.Sendf("Please, use /start ")
|
||||||
|
})
|
||||||
|
|
||||||
|
var BotCommands = []tg.Command{
|
||||||
|
tg.NewCommand(
|
||||||
|
"start",
|
||||||
|
"start or restart the bot or move to the start screen",
|
||||||
|
).Go(StartWidget),
|
||||||
|
tg.NewCommand(
|
||||||
|
"info",
|
||||||
|
"info desc",
|
||||||
|
).WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
c.SendfHTML(`<a href="https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg">cock</a><strong>cock</strong> die`)
|
||||||
|
})),
|
||||||
|
tg.NewCommand("hello", "sends the 'Hello, World!' message back").
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
c.Sendf("Hello, World!")
|
||||||
|
})),
|
||||||
|
tg.NewCommand("read", "reads a string and sends it back").
|
||||||
|
WithWidget(
|
||||||
|
tg.Func(func(c tg.Context) {
|
||||||
|
str := c.ReadString("Type a string and I will send it back")
|
||||||
|
if str == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Sendf2("You typed `%s`", str)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
tg.NewCommand("cat", "sends a sample image of cat from the server storage").
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
f, err := os.Open("media/cat.jpg")
|
||||||
|
if err != nil {
|
||||||
|
c.Sendf("err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
photo := tg.NewFile(f).Photo().Name("cat.jpg").Caption("A cat!")
|
||||||
|
c.Send(photo)
|
||||||
|
})),
|
||||||
|
tg.NewCommand("document", "sends a sample text document").
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
f, err := os.Open("media/hello.txt")
|
||||||
|
if err != nil {
|
||||||
|
c.Sendf("err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
doc := tg.NewFile(f).Document().Name("hello.txt").Caption("The document")
|
||||||
|
c.Send(doc)
|
||||||
|
})),
|
||||||
|
tg.NewCommand("botname", "get the bot name").
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
bd := c.Bot().Data().(*BotData)
|
||||||
|
c.Sendf("My name is %q", bd.Name)
|
||||||
|
})),
|
||||||
|
tg.NewCommand("history", "print go history").
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
c.Sendf("%q", c.PathHistory())
|
||||||
|
})),
|
||||||
|
tg.NewCommand(
|
||||||
|
"washington",
|
||||||
|
"send location of the Washington",
|
||||||
|
).WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
c.Sendf("Washington location")
|
||||||
|
c.Send(
|
||||||
|
tg.Messagef("").Location(
|
||||||
|
47.751076, -120.740135,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
}
|
57
cmd/test/incdec.go
Normal file
57
cmd/test/incdec.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A simple example widget to show
|
||||||
|
// how to store and get session data values
|
||||||
|
// and working with dynamic panels.
|
||||||
|
var IncDecWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
const format = "Press the buttons" +
|
||||||
|
"to increment and decrement.\n" +
|
||||||
|
"Current counter value = %d"
|
||||||
|
d := ExtractSessionData(c)
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(format, d.Counter).Panel(
|
||||||
|
c,
|
||||||
|
tg.PanelFunc(func(
|
||||||
|
panel *tg.PanelCompo,
|
||||||
|
c tg.Context,
|
||||||
|
) []tg.ButtonRow {
|
||||||
|
d := ExtractSessionData(c)
|
||||||
|
row := tg.ButtonRow{}
|
||||||
|
if d.Counter != -5 {
|
||||||
|
row = append(
|
||||||
|
row,
|
||||||
|
tg.Buttonf(
|
||||||
|
"-",
|
||||||
|
).WithAction(tg.Func(func(c tg.Context){
|
||||||
|
d.Counter--
|
||||||
|
panel.Text = fmt.Sprintf(format, d.Counter)
|
||||||
|
c.Update(panel)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if d.Counter != +5 {
|
||||||
|
row = append(
|
||||||
|
row,
|
||||||
|
tg.Buttonf(
|
||||||
|
"+",
|
||||||
|
).WithAction(tg.Func(func(c tg.Context){
|
||||||
|
d.Counter++
|
||||||
|
panel.Text = fmt.Sprintf(format, d.Counter)
|
||||||
|
c.Update(panel)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []tg.ButtonRow{row}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
tg.Messagef("Use the reply keyboard to get back").Reply(
|
||||||
|
BackKeyboard.Reply(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
30
cmd/test/keyboard.go
Normal file
30
cmd/test/keyboard.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var HomeButton = tg.Buttonf("Home").Go(StartWidget)
|
||||||
|
var BackButton = tg.Buttonf("Back").Go(tg.Back)
|
||||||
|
var BackKeyboard = tg.NewKeyboard().Row(
|
||||||
|
BackButton,
|
||||||
|
)
|
||||||
|
|
||||||
|
var SendLocationKeyboard = tg.NewKeyboard().Row(
|
||||||
|
tg.Buttonf("Send location").
|
||||||
|
WithSendLocation(true).
|
||||||
|
WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
l := c.CallbackUpdate().Message.Location
|
||||||
|
c.Sendf(
|
||||||
|
"Longitude: %f\n"+
|
||||||
|
"Latitude: %f\n"+
|
||||||
|
"Heading: %d"+
|
||||||
|
"",
|
||||||
|
l.Longitude,
|
||||||
|
l.Latitude,
|
||||||
|
l.Heading,
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
).Row(
|
||||||
|
BackButton,
|
||||||
|
).Reply()
|
30
cmd/test/location.go
Normal file
30
cmd/test/location.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LocationWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
"Press the button to display your counter",
|
||||||
|
).Inline(
|
||||||
|
tg.NewKeyboard().Row(
|
||||||
|
tg.Buttonf(
|
||||||
|
"Check",
|
||||||
|
).WithData(
|
||||||
|
"check",
|
||||||
|
).WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
d := ExtractSessionData(c)
|
||||||
|
c.Sendf("Counter = %d", d.Counter)
|
||||||
|
})),
|
||||||
|
).Inline(),
|
||||||
|
),
|
||||||
|
|
||||||
|
tg.Messagef(
|
||||||
|
"Press the button to send your location!",
|
||||||
|
).Reply(
|
||||||
|
SendLocationKeyboard,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
402
cmd/test/main.go
402
cmd/test/main.go
|
@ -3,10 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"vultras.su/core/tg"
|
"surdeus.su/core/tg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BotData struct {
|
type BotData struct {
|
||||||
|
@ -17,393 +15,47 @@ type SessionData struct {
|
||||||
Counter int
|
Counter int
|
||||||
}
|
}
|
||||||
|
|
||||||
type MutateMessageWidget struct {
|
func ExtractSessionData(c tg.Context) *SessionData {
|
||||||
Mutate func(string) string
|
return c.SessionData().(*SessionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
|
var BackWidget = tg.RenderFunc(func(c tg.Context) tg.UI{
|
||||||
ret := &MutateMessageWidget{}
|
return c.GoRet(tg.Back)
|
||||||
ret.Mutate = fn
|
})
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *MutateMessageWidget) Serve(c *tg.Context) {
|
var beh = tg.NewBehaviour().SetInit(tg.Func(func(c tg.Context) {
|
||||||
args, ok := c.Arg().([]any)
|
// The session initialization.
|
||||||
if ok {
|
c.SetSessionData(&SessionData{})
|
||||||
for _, arg := range args {
|
})).SetRootWidget(
|
||||||
c.Sendf("%v", arg)
|
// Setting as the most top
|
||||||
}
|
// widget command handling
|
||||||
}
|
// so we can call them at any screen.
|
||||||
for u := range c.Input() {
|
tg.NewCommandCompo().SetUsage(
|
||||||
text := u.Message.Text
|
UsageAction,
|
||||||
_, err := c.Sendf2("%s", w.Mutate(text))
|
).SetPreStart(
|
||||||
if err != nil {
|
PreStartAction,
|
||||||
c.Sendf("debug: %q", err)
|
).SetCommands(
|
||||||
}
|
BotCommands...,
|
||||||
}
|
),
|
||||||
}
|
|
||||||
|
|
||||||
func (w *MutateMessageWidget) Filter(u *tg.Update) bool {
|
|
||||||
if u.Message == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractSessionData(c *tg.Context) *SessionData {
|
|
||||||
return c.Session.Data.(*SessionData)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
homeButton = tg.NewButton("Home").Go("/")
|
|
||||||
backButton = tg.NewButton("Back").Go("-")
|
|
||||||
backKeyboard = tg.NewKeyboard().Row(
|
|
||||||
backButton,
|
|
||||||
)
|
|
||||||
|
|
||||||
sendLocationKeyboard = tg.NewKeyboard().Row(
|
|
||||||
tg.NewButton("Send location").
|
|
||||||
WithSendLocation(true).
|
|
||||||
ActionFunc(func(c *tg.Context) {
|
|
||||||
l := c.Message.Location
|
|
||||||
c.Sendf(
|
|
||||||
"Longitude: %f\n"+
|
|
||||||
"Latitude: %f\n"+
|
|
||||||
"Heading: %d"+
|
|
||||||
"",
|
|
||||||
l.Longitude,
|
|
||||||
l.Latitude,
|
|
||||||
l.Heading,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
).Row(
|
|
||||||
backButton,
|
|
||||||
).Reply()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var beh = tg.NewBehaviour().
|
|
||||||
WithInitFunc(func(c *tg.Context) {
|
|
||||||
// The session initialization.
|
|
||||||
c.Session.Data = &SessionData{}
|
|
||||||
}).WithRootNode(tg.NewRootNode(
|
|
||||||
// The "/" widget.
|
|
||||||
tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(fmt.Sprintf(
|
|
||||||
fmt.Sprint(
|
|
||||||
"Hello, %s!\n",
|
|
||||||
"The testing bot started!\n",
|
|
||||||
"You can see the basics of usage in the ",
|
|
||||||
"cmd/test/main.go file!",
|
|
||||||
),
|
|
||||||
c.SentFrom().UserName,
|
|
||||||
)).Inline(
|
|
||||||
tg.NewKeyboard().Row(
|
|
||||||
tg.NewButton("GoT Github page").
|
|
||||||
WithUrl("https://github.com/mojosa-software/got"),
|
|
||||||
).Inline(),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.NewMessage("Choose your interest").Reply(
|
|
||||||
tg.NewKeyboard().Row(
|
|
||||||
tg.NewButton("Inc/Dec").Go("/inc-dec"),
|
|
||||||
).Row(
|
|
||||||
tg.NewButton("Mutate messages").Go("/mutate-messages"),
|
|
||||||
).Row(
|
|
||||||
tg.NewButton("Send location").Go("/send-location"),
|
|
||||||
).Row(
|
|
||||||
tg.NewButton("Dynamic panel").Go("panel"),
|
|
||||||
).Reply(),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.Func(func(c *tg.Context) {
|
|
||||||
for u := range c.Input() {
|
|
||||||
if u.EditedMessage != nil {
|
|
||||||
c.Sendf2("The new message is `%s`", u.EditedMessage.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
tg.NewNode(
|
|
||||||
"panel",
|
|
||||||
tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
var (
|
|
||||||
n = 0
|
|
||||||
ln = 4
|
|
||||||
panel *tg.PanelCompo
|
|
||||||
)
|
|
||||||
|
|
||||||
panel = tg.NewMessage(
|
|
||||||
"Some panel",
|
|
||||||
).Panel(c, tg.RowserFunc(func(c *tg.Context) []tg.ButtonRow {
|
|
||||||
btns := []tg.ButtonRow{
|
|
||||||
tg.ButtonRow{tg.NewButton("Static shit")},
|
|
||||||
}
|
|
||||||
for i := 0; i < ln; i++ {
|
|
||||||
num := 1 + n*ln + i
|
|
||||||
btns = append(btns, tg.ButtonRow{
|
|
||||||
tg.NewButton("%d", num).WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
c.Sendf("%d", num*num)
|
|
||||||
})),
|
|
||||||
tg.NewButton("%d", num*num),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
btns = append(btns, tg.ButtonRow{
|
|
||||||
tg.NewButton("Prev").WithAction(tg.ActionFunc(func(c *tg.Context) {
|
|
||||||
n--
|
|
||||||
panel.Update(c)
|
|
||||||
})),
|
|
||||||
tg.NewButton("Next").WithAction(tg.ActionFunc(func(c *tg.Context) {
|
|
||||||
n++
|
|
||||||
panel.Update(c)
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
return btns
|
|
||||||
}))
|
|
||||||
|
|
||||||
return tg.UI{
|
|
||||||
panel,
|
|
||||||
tg.NewMessage("").Reply(
|
|
||||||
backKeyboard.Reply(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.NewNode(
|
|
||||||
"mutate-messages", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(
|
|
||||||
"Choose the function to mutate string",
|
|
||||||
).Reply(
|
|
||||||
tg.NewKeyboard().Row(
|
|
||||||
tg.NewButton("Upper case").Go("upper-case"),
|
|
||||||
tg.NewButton("Lower case").Go("lower-case"),
|
|
||||||
tg.NewButton("Escape chars").Go("escape"),
|
|
||||||
).Row(
|
|
||||||
backButton,
|
|
||||||
).Reply(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
tg.NewNode(
|
|
||||||
"upper-case", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(
|
|
||||||
"Type a string and the bot will convert it to upper case",
|
|
||||||
).Reply(
|
|
||||||
backKeyboard.Reply(),
|
|
||||||
),
|
|
||||||
NewMutateMessageWidget(strings.ToUpper),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tg.NewNode(
|
|
||||||
"lower-case", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(
|
|
||||||
"Type a string and the bot will convert it to lower case",
|
|
||||||
).Reply(
|
|
||||||
backKeyboard.Reply(),
|
|
||||||
),
|
|
||||||
NewMutateMessageWidget(strings.ToLower),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tg.NewNode(
|
|
||||||
"escape", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(
|
|
||||||
"Type a string and the bot will escape characters in it",
|
|
||||||
).Reply(
|
|
||||||
backKeyboard.Reply(),
|
|
||||||
),
|
|
||||||
NewMutateMessageWidget(tg.Escape2),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.NewNode(
|
|
||||||
"inc-dec", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
var (
|
|
||||||
kbd *tg.InlineCompo
|
|
||||||
//cntMsg *tg.MessageCompo
|
|
||||||
inline, std, onlyInc, onlyDec *tg.Inline
|
|
||||||
)
|
|
||||||
|
|
||||||
d := ExtractSessionData(c)
|
|
||||||
format := "Press the buttons to increment and decrement.\n" +
|
|
||||||
"Current counter value = %d"
|
|
||||||
|
|
||||||
incBtn := tg.NewButton("+").ActionFunc(func(c *tg.Context) {
|
|
||||||
d.Counter++
|
|
||||||
kbd.Text = fmt.Sprintf(format, d.Counter)
|
|
||||||
if d.Counter == 5 {
|
|
||||||
kbd.Inline = onlyDec
|
|
||||||
} else {
|
|
||||||
kbd.Inline = std
|
|
||||||
}
|
|
||||||
kbd.Update(c)
|
|
||||||
})
|
|
||||||
decBtn := tg.NewButton("-").ActionFunc(func(c *tg.Context) {
|
|
||||||
d.Counter--
|
|
||||||
kbd.Text = fmt.Sprintf(format, d.Counter)
|
|
||||||
if d.Counter == -5 {
|
|
||||||
kbd.Inline = onlyInc
|
|
||||||
} else {
|
|
||||||
kbd.Inline = std
|
|
||||||
}
|
|
||||||
kbd.Update(c)
|
|
||||||
//c.Sendf("%d", d.Counter)
|
|
||||||
})
|
|
||||||
|
|
||||||
onlyInc = tg.NewKeyboard().Row(incBtn).Inline()
|
|
||||||
onlyDec = tg.NewKeyboard().Row(decBtn).Inline()
|
|
||||||
std = tg.NewKeyboard().Row(incBtn, decBtn).Inline()
|
|
||||||
|
|
||||||
if d.Counter == 5 {
|
|
||||||
inline = onlyDec
|
|
||||||
} else if d.Counter == -5 {
|
|
||||||
inline = onlyInc
|
|
||||||
} else {
|
|
||||||
inline = std
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd = tg.NewMessage(
|
|
||||||
fmt.Sprintf(format, d.Counter),
|
|
||||||
).Inline(inline)
|
|
||||||
|
|
||||||
return tg.UI{
|
|
||||||
kbd,
|
|
||||||
tg.NewMessage("Use the reply keyboard to get back").Reply(
|
|
||||||
backKeyboard.Reply(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.NewNode(
|
|
||||||
"send-location", tg.RenderFunc(func(c *tg.Context) tg.UI {
|
|
||||||
return tg.UI{
|
|
||||||
tg.NewMessage(
|
|
||||||
"Press the button to display your counter",
|
|
||||||
).Inline(
|
|
||||||
tg.NewKeyboard().Row(
|
|
||||||
tg.NewButton(
|
|
||||||
"Check",
|
|
||||||
).WithData(
|
|
||||||
"check",
|
|
||||||
).WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
d := ExtractSessionData(c)
|
|
||||||
c.Sendf("Counter = %d", d.Counter)
|
|
||||||
})),
|
|
||||||
).Inline(),
|
|
||||||
),
|
|
||||||
|
|
||||||
tg.NewMessage(
|
|
||||||
"Press the button to send your location!",
|
|
||||||
).Reply(
|
|
||||||
sendLocationKeyboard,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)).WithRoot(tg.NewCommandCompo().
|
|
||||||
WithUsage(tg.Func(func(c *tg.Context) {
|
|
||||||
c.Sendf("There is no such command %q", c.Message.Command())
|
|
||||||
})).WithPreStart(tg.Func(func(c *tg.Context) {
|
|
||||||
c.Sendf("Please, use /start ")
|
|
||||||
})).WithCommands(
|
|
||||||
tg.NewCommand("info", "info desc").
|
|
||||||
ActionFunc(func(c *tg.Context) {
|
|
||||||
c.SendfHTML(`<a href="https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg">cock</a><strong>cock</strong> die`)
|
|
||||||
}),
|
|
||||||
tg.NewCommand(
|
|
||||||
"start",
|
|
||||||
"start or restart the bot or move to the start screen",
|
|
||||||
).Go("/"),
|
|
||||||
tg.NewCommand("hello", "sends the 'Hello, World!' message back").
|
|
||||||
ActionFunc(func(c *tg.Context) {
|
|
||||||
c.Sendf("Hello, World!")
|
|
||||||
}),
|
|
||||||
tg.NewCommand("read", "reads a string and sends it back").
|
|
||||||
WithWidget(
|
|
||||||
tg.Func(func(c *tg.Context) {
|
|
||||||
str := c.ReadString("Type a string and I will send it back")
|
|
||||||
c.Sendf2("You typed `%s`", str)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tg.NewCommand("cat", "sends a sample image of cat").
|
|
||||||
ActionFunc(func(c *tg.Context) {
|
|
||||||
f, err := os.Open("media/cat.jpg")
|
|
||||||
if err != nil {
|
|
||||||
c.Sendf("err: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
photo := tg.NewFile(f).Photo().Name("cat.jpg").Caption("A cat!")
|
|
||||||
c.Send(photo)
|
|
||||||
}),
|
|
||||||
tg.NewCommand("document", "sends a sample text document").
|
|
||||||
ActionFunc(func(c *tg.Context) {
|
|
||||||
f, err := os.Open("media/hello.txt")
|
|
||||||
if err != nil {
|
|
||||||
c.Sendf("err: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
doc := tg.NewFile(f).Document().Name("hello.txt").Caption("The document")
|
|
||||||
c.Send(doc)
|
|
||||||
}),
|
|
||||||
tg.NewCommand("botname", "get the bot name").
|
|
||||||
WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
bd := c.Bot.Data.(*BotData)
|
|
||||||
c.Sendf("My name is %q", bd.Name)
|
|
||||||
})),
|
|
||||||
tg.NewCommand("dynamic", "check of the dynamic work").
|
|
||||||
WithWidget(tg.Func(func(c *tg.Context) {
|
|
||||||
})),
|
|
||||||
tg.NewCommand("history", "print go history").
|
|
||||||
WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
c.Sendf("%q", c.History())
|
|
||||||
})),
|
|
||||||
tg.NewCommand("washington", "send location of the Washington").
|
|
||||||
WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
c.Sendf("Washington location")
|
|
||||||
c.Send(
|
|
||||||
tg.NewMessage("").Location(
|
|
||||||
47.751076, -120.740135,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
tg.NewCommand("invoice", "invoice check").
|
|
||||||
WithAction(tg.Func(func(c *tg.Context) {
|
|
||||||
})),
|
|
||||||
))
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println(beh.Screens)
|
|
||||||
token := os.Getenv("BOT_TOKEN")
|
token := os.Getenv("BOT_TOKEN")
|
||||||
|
|
||||||
bot, err := tg.NewBot(token)
|
bot, err := tg.NewBot(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatalf("tg.NewBot(...): %s", err)
|
||||||
}
|
}
|
||||||
bot = bot.
|
bot = bot.SetBehaviour(beh)
|
||||||
WithBehaviour(beh).
|
//bot.API().Debug = true
|
||||||
Debug(true)
|
|
||||||
|
|
||||||
bot.Data = &BotData{
|
bot.SetData(&BotData{
|
||||||
Name: "Jay",
|
Name: "Jay",
|
||||||
}
|
})
|
||||||
|
|
||||||
log.Printf("Authorized on account %s", bot.Api.Self.UserName)
|
log.Printf("Authorized on account %s", bot.API().Self.UserName)
|
||||||
err = bot.Run()
|
err = bot.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("bot.Run(...): %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
92
cmd/test/mutate.go
Normal file
92
cmd/test/mutate.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The component to get incoming messages and
|
||||||
|
// send back mutated version.
|
||||||
|
type MutateMessageCompo struct {
|
||||||
|
Mutate func(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMutateMessageCompo(fn func(string) string) *MutateMessageCompo {
|
||||||
|
ret := &MutateMessageCompo{}
|
||||||
|
ret.Mutate = fn
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MutateMessageCompo) Serve(c tg.Context) {
|
||||||
|
args, ok := c.Arg().([]any)
|
||||||
|
if ok {
|
||||||
|
for _, arg := range args {
|
||||||
|
c.Sendf("%v", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for u := range c.Input() {
|
||||||
|
text := u.Message.Text
|
||||||
|
_, err := c.Sendf2("%s", w.Mutate(text))
|
||||||
|
if err != nil {
|
||||||
|
c.Sendf("debug: %q", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementing the Filter interface making
|
||||||
|
// possible to give the updates away for
|
||||||
|
// the underlying components.
|
||||||
|
func (w *MutateMessageCompo) Filter(u tg.Update) bool {
|
||||||
|
if u.Message == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var MutateMessagesWidget= tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
"Choose widget to mutate strings",
|
||||||
|
).Reply(
|
||||||
|
tg.NewKeyboard().Row(
|
||||||
|
tg.Buttonf("Upper case").Go(UpperCaseWidget),
|
||||||
|
tg.Buttonf("Lower case").Go(LowerCaseWidget),
|
||||||
|
tg.Buttonf("Escape chars").Go(EscapeWidget),
|
||||||
|
).Row(
|
||||||
|
BackButton,
|
||||||
|
).Reply(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var LowerCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
"Type a string and the bot will convert it to lower case",
|
||||||
|
).Reply(
|
||||||
|
BackKeyboard.Reply(),
|
||||||
|
),
|
||||||
|
NewMutateMessageCompo(strings.ToLower),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var UpperCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
"Type a string and the bot will convert it to upper case",
|
||||||
|
).Reply(
|
||||||
|
BackKeyboard.Reply(),
|
||||||
|
),
|
||||||
|
NewMutateMessageCompo(strings.ToUpper),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var EscapeWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
"Type a string and the bot will escape characters in it",
|
||||||
|
).Reply(
|
||||||
|
BackKeyboard.Reply(),
|
||||||
|
),
|
||||||
|
NewMutateMessageCompo(tg.Escape2),
|
||||||
|
}
|
||||||
|
})
|
38
cmd/test/panel.go
Normal file
38
cmd/test/panel.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DynamicPanelWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef("Paged panel").PanelPager(
|
||||||
|
c, 0, 5,
|
||||||
|
tg.PanelPagerFunc(func(
|
||||||
|
panel *tg.PanelPagerCompo,
|
||||||
|
c tg.Context, page, size int,
|
||||||
|
) tg.PanelPage {
|
||||||
|
rows := []tg.ButtonRow{}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
num := 1 + page*size + i
|
||||||
|
rows = append(rows, tg.ButtonRow{
|
||||||
|
tg.Buttonf("%d", num).Rand().WithAction(tg.Func(func(c tg.Context) {
|
||||||
|
_, err := c.Sendf("%d", num*num)
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
tg.Buttonf("%d", num*num),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tg.PanelPage{
|
||||||
|
Rows: rows,
|
||||||
|
Next: page < 3,
|
||||||
|
Prev: page != 0,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
tg.Messagef("").Reply(
|
||||||
|
BackKeyboard.Reply(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
45
cmd/test/start.go
Normal file
45
cmd/test/start.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/tg"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var StartWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
|
||||||
|
return tg.UI{
|
||||||
|
tg.Messagef(
|
||||||
|
fmt.Sprint(
|
||||||
|
"Hello, %s!",
|
||||||
|
"The testing bot started!",
|
||||||
|
"You can see the basics of usage in the ",
|
||||||
|
"cmd/test/main.go file and other files in the cmd/test!",
|
||||||
|
),
|
||||||
|
c.CallbackUpdate().SentFrom().UserName,
|
||||||
|
).Inline(
|
||||||
|
tg.NewKeyboard().Row(
|
||||||
|
tg.Buttonf("TeleGopher surdeus.su page").
|
||||||
|
WithURL("https://surdeus.su/core/tg"),
|
||||||
|
).Inline(),
|
||||||
|
),
|
||||||
|
|
||||||
|
tg.Messagef("Choose your interest").Reply(
|
||||||
|
tg.NewKeyboard().List(
|
||||||
|
tg.Buttonf("Back").Go(BackWidget),
|
||||||
|
tg.Buttonf("Inc/Dec").Go(IncDecWidget),
|
||||||
|
tg.Buttonf("Mutate messages").Go(MutateMessagesWidget),
|
||||||
|
tg.Buttonf("Send location").Go(LocationWidget),
|
||||||
|
tg.Buttonf("Dynamic panel").Go(DynamicPanelWidget),
|
||||||
|
tg.Buttonf("Check panic").Go(nil),
|
||||||
|
).Reply(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Testing reaction to editing messages.
|
||||||
|
tg.Func(func(c tg.Context) {
|
||||||
|
for u := range c.Input() {
|
||||||
|
if u.EditedMessage != nil {
|
||||||
|
c.Sendf2("The new message is `%s`", u.EditedMessage.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
73
command.go
73
command.go
|
@ -4,6 +4,7 @@ import (
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type CommandType uint8
|
type CommandType uint8
|
||||||
const (
|
const (
|
||||||
PrivateCommandType CommandType = iota
|
PrivateCommandType CommandType = iota
|
||||||
|
@ -18,21 +19,22 @@ type Command struct {
|
||||||
Description string
|
Description string
|
||||||
Action Action
|
Action Action
|
||||||
Widget Widget
|
Widget Widget
|
||||||
|
WidgetArg any
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandMap map[CommandName]*Command
|
type CommandMap map[CommandName]Command
|
||||||
|
|
||||||
func NewCommand(name CommandName, desc string) Command {
|
func NewCommand(name CommandName, desc string) Command {
|
||||||
if name == "" || desc == "" {
|
if name == "" || desc == "" {
|
||||||
panic("name and description cannot be an empty string")
|
panic("name and description cannot be an empty string")
|
||||||
}
|
}
|
||||||
return &Command{
|
return Command{
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) WithAction(a Action) *Command {
|
func (c Command) WithAction(a Action) Command {
|
||||||
c.Action = a
|
c.Action = a
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
@ -42,21 +44,25 @@ func (c Command) WithWidget(w Widget) Command {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) WidgetFunc(fn Func) Command {
|
// Convert command into the tgbotapi.BotCommand
|
||||||
return c.WithWidget(fn)
|
func (c Command) ToAPI() tgbotapi.BotCommand {
|
||||||
}
|
|
||||||
|
|
||||||
func (c Command) ToApi() tgbotapi.BotCommand {
|
|
||||||
ret := tgbotapi.BotCommand{}
|
ret := tgbotapi.BotCommand{}
|
||||||
ret.Command = string(c.Name)
|
ret.Command = string(c.Name)
|
||||||
ret.Description = c.Description
|
ret.Description = c.Description
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) Go(pth Path, args ...any) Command {
|
// Simple command to go to another screen.
|
||||||
return c.WithAction(ScreenGo{
|
func (c Command) Go(pth Widget) Command {
|
||||||
|
return c.WithAction(WidgetGo{
|
||||||
Path: pth,
|
Path: pth,
|
||||||
Args: args,
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) GoWithArg(pth Widget, arg any) Command {
|
||||||
|
return c.WithAction(WidgetGo{
|
||||||
|
Path: pth,
|
||||||
|
Arg: arg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,13 +75,13 @@ type CommandCompo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns new empty CommandCompo.
|
// Returns new empty CommandCompo.
|
||||||
func NewCommandCompo(cmds ...*Command) *CommandCompo {
|
func NewCommandCompo(cmds ...Command) *CommandCompo {
|
||||||
ret := CommandCompo{}.WithCommands(cmds...)
|
ret := (&CommandCompo{}).SetCommands(cmds...)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the commands to handle.
|
// Set the commands to handle.
|
||||||
func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo {
|
func (w *CommandCompo) SetCommands(cmds ...Command) *CommandCompo {
|
||||||
if w.Commands == nil {
|
if w.Commands == nil {
|
||||||
w.Commands = make(CommandMap)
|
w.Commands = make(CommandMap)
|
||||||
}
|
}
|
||||||
|
@ -93,24 +99,20 @@ func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the prestart action.
|
// Set the prestart action.
|
||||||
func (w *CommandCompo) WithPreStart(a Action) *CommandCompo {
|
func (w *CommandCompo) SetPreStart(a Action) *CommandCompo {
|
||||||
w.PreStart = a
|
w.PreStart = a
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the usage action.
|
// Set the usage action.
|
||||||
func (w CommandCompo) WithUsage(a Action) *CommandCompo {
|
func (w *CommandCompo) SetUsage(a Action) *CommandCompo {
|
||||||
w.Usage = a
|
w.Usage = a
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the usage action with function.
|
// Filtering all the non commands.
|
||||||
func (w CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo {
|
func (widget *CommandCompo) Filter(
|
||||||
return w.WithUsage(fn)
|
u Update,
|
||||||
}
|
|
||||||
|
|
||||||
func (widget CommandCompo) Filter(
|
|
||||||
u *Update,
|
|
||||||
) bool {
|
) bool {
|
||||||
if u.Message == nil || !u.Message.IsCommand() {
|
if u.Message == nil || !u.Message.IsCommand() {
|
||||||
return false
|
return false
|
||||||
|
@ -120,27 +122,26 @@ func (widget CommandCompo) Filter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing server.
|
// Implementing server.
|
||||||
func (compo CommandCompo) Serve(c Context) {
|
func (compo *CommandCompo) Serve(c Context) {
|
||||||
/*commanders := make(map[CommandName] BotCommander)
|
// First should bring the new command into the action.
|
||||||
for k, v := range compo.Commands {
|
c.Bot().DeleteCommands()
|
||||||
commanders[k] = v
|
err := c.Bot().SetCommands(
|
||||||
}*/
|
tgbotapi.NewBotCommandScopeChat(c.SessionID().ToAPI()),
|
||||||
c.bot.DeleteCommands()
|
|
||||||
err := c.bot.SetCommands(
|
|
||||||
tgbotapi.NewBotCommandScopeChat(c.Session.Id.ToApi()),
|
|
||||||
compo.Commands,
|
compo.Commands,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Sendf("error: %q", err)
|
c.Sendf("error: %q", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdUpdates *UpdateChan
|
var cmdUpdates *UpdateChan
|
||||||
for u := range c.Input() {
|
for u := range c.Input() {
|
||||||
if c.Path() == "" && u.Message != nil {
|
if c.Path() == nil && u.Message != nil {
|
||||||
// Skipping and executing the preinit action
|
// Skipping and executing the preinit action
|
||||||
// while we have the empty screen.
|
// while we have the empty screen.
|
||||||
// E. g. the session did not start.
|
// E. g. the session did not start.
|
||||||
if !(u.Message.IsCommand() && u.Message.Command() == "start") {
|
if !u.Message.IsCommand() ||
|
||||||
|
u.Message.Command() != "start" {
|
||||||
c.WithUpdate(u).Run(compo.PreStart)
|
c.WithUpdate(u).Run(compo.PreStart)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -157,8 +158,10 @@ func (compo CommandCompo) Serve(c Context) {
|
||||||
|
|
||||||
c.WithUpdate(u).Run(cmd.Action)
|
c.WithUpdate(u).Run(cmd.Action)
|
||||||
if cmd.Widget != nil {
|
if cmd.Widget != nil {
|
||||||
|
// Closing current widget
|
||||||
cmdUpdates.Close()
|
cmdUpdates.Close()
|
||||||
cmdUpdates, _ = c.WithUpdate(u).RunWidget(cmd.Widget)
|
// And running the other one.
|
||||||
|
cmdUpdates, _ = c.WithArg(cmd.WidgetArg).RunWidget(cmd.Widget)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -168,7 +171,7 @@ func (compo CommandCompo) Serve(c Context) {
|
||||||
// executing one.
|
// executing one.
|
||||||
cmdUpdates.Send(u)
|
cmdUpdates.Send(u)
|
||||||
} else {
|
} else {
|
||||||
c.Skip(u)
|
c.SkipUpdate(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
288
context.go
288
context.go
|
@ -6,11 +6,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
//"path"
|
//"path"
|
||||||
|
//"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// Interface to interact with the user.
|
// Interface to interact with the user.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
// The session contains all
|
||||||
|
// the information between the contexts.
|
||||||
session *Session
|
session *Session
|
||||||
// The update that called the Context usage.
|
// The update that called the Context usage.
|
||||||
update Update
|
update Update
|
||||||
|
@ -27,15 +30,15 @@ type Context struct {
|
||||||
// make other user to leave the bot at first but
|
// make other user to leave the bot at first but
|
||||||
// maybe you will find another usage for this.
|
// maybe you will find another usage for this.
|
||||||
// Returns users context by specified session ID
|
// Returns users context by specified session ID
|
||||||
// or nil if the user is not logged in.
|
// or false if the user is not logged in.
|
||||||
func (c Context) As(sid SessionId) Context {
|
func (c Context) As(sid SessionID) (Context, bool) {
|
||||||
n, ok := c.Bot.contexts[sid]
|
s, ok := c.Bot().GotSession(sid)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return Context{}, false
|
||||||
}
|
|
||||||
return &Context{
|
|
||||||
context: n,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.session = s
|
||||||
|
return c, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// General type function to define actions, single component widgets
|
// General type function to define actions, single component widgets
|
||||||
|
@ -47,15 +50,18 @@ func (f Func) Act(c Context) {
|
||||||
func (f Func) Serve(c Context) {
|
func (f Func) Serve(c Context) {
|
||||||
f(c)
|
f(c)
|
||||||
}
|
}
|
||||||
func(f Func) Filter(_ *Update) bool {
|
func(f Func) Filter(_ Update) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func (f Func) Render(_ *Context) UI {
|
func (f Func) Render(_ Context) UI {
|
||||||
return UI{
|
return UI{
|
||||||
f,
|
f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The type represents type
|
||||||
|
// of current context the processing is happening
|
||||||
|
// in.
|
||||||
type ContextType uint8
|
type ContextType uint8
|
||||||
const (
|
const (
|
||||||
NoContextType ContextType = iota
|
NoContextType ContextType = iota
|
||||||
|
@ -65,17 +71,16 @@ const (
|
||||||
|
|
||||||
// Goroutie function to handle each user.
|
// Goroutie function to handle each user.
|
||||||
func (c Context) serve() {
|
func (c Context) serve() {
|
||||||
beh := c.Bot.behaviour
|
beh := c.Bot().behaviour
|
||||||
c.Run(beh.Init)
|
c.Run(beh.Init)
|
||||||
beh.Root.Serve(c)
|
for {
|
||||||
}
|
defer func(){
|
||||||
|
if err := recover() ; err != nil {
|
||||||
func (c Context) Path() Path {
|
// Need to add some handling later.
|
||||||
ln := len(c.pathHistory)
|
}
|
||||||
if ln == 0 {
|
}()
|
||||||
return ""
|
beh.Root.Serve(c)
|
||||||
}
|
}
|
||||||
return c.pathHistory[ln-1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) Arg() any {
|
func (c Context) Arg() any {
|
||||||
|
@ -88,21 +93,15 @@ func (c Context) Run(a Action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only for the root widget usage.
|
|
||||||
// Skip the update sending it down to
|
|
||||||
// the underlying widget.
|
|
||||||
func (c Context) Skip(u Update) {
|
|
||||||
c.skippedUpdates.Send(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends to the Sendable object.
|
// Sends to the Sendable object to the session user.
|
||||||
func (c Context) Send(v Sendable) (Message, error) {
|
func (c Context) Send(v Sendable) (*Message, error) {
|
||||||
config := v.SendConfig(c.Session.Id, c.Bot)
|
config := v.SendConfig(c.SessionID(), c.Bot())
|
||||||
if config.Error != nil {
|
if config.Error != nil {
|
||||||
return nil, config.Error
|
return nil, config.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := c.Bot.Api.Send(config.ToApi())
|
msg, err := c.Bot().API().Send(config.ToAPI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -111,23 +110,23 @@ func (c Context) Send(v Sendable) (Message, error) {
|
||||||
|
|
||||||
// Sends the formatted with fmt.Sprintf message to the user
|
// Sends the formatted with fmt.Sprintf message to the user
|
||||||
// using default Markdown parsing format.
|
// using default Markdown parsing format.
|
||||||
func (c Context) Sendf(format string, v ...any) (Message, error) {
|
func (c Context) Sendf(format string, v ...any) (*Message, error) {
|
||||||
return c.Send(NewMessage(format, v...))
|
return c.Send(Messagef(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as Sendf but uses Markdown 2 format for parsing.
|
// Same as Sendf but uses Markdown 2 format for parsing.
|
||||||
func (c Context) Sendf2(format string, v ...any) (Message, error) {
|
func (c Context) Sendf2(format string, v ...any) (*Message, error) {
|
||||||
return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2())
|
return c.Send(Messagef(format, v...).MD2())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as Sendf but uses HTML format for parsing.
|
// Same as Sendf but uses HTML format for parsing.
|
||||||
func (c Context) SendfHTML(format string, v ...any) (Message, error) {
|
func (c Context) SendfHTML(format string, v ...any) (*Message, error) {
|
||||||
return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML())
|
return c.Send(Messagef(format, v...).HTML())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message in raw format escaping all the special characters.
|
// Send the message in raw format escaping all the special characters.
|
||||||
func (c Context) SendfR(format string, v ...any) (Message, error) {
|
func (c Context) SendfR(format string, v ...any) (*Message, error) {
|
||||||
return c.Send(NewMessage(Escape2(fmt.Sprintf(format, v...))).MD2())
|
return c.Send(Messagef("%s", Escape2(fmt.Sprintf(format, v...))).MD2())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the input for current widget.
|
// Get the input for current widget.
|
||||||
|
@ -141,8 +140,8 @@ func (c Context) WithArg(v any) Context {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) WithUpdate(u *Update) Context {
|
func (c Context) WithUpdate(u Update) Context {
|
||||||
c.Update = u
|
c.update = u
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,14 +150,6 @@ func (c Context) WithInput(input *UpdateChan) Context {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) Go(pth Path) error {
|
|
||||||
return c.session.go_(pth, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Context) GoWithArg(pth Path, arg any) error {
|
|
||||||
return c.session.go_(pth, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Customized actions for the bot.
|
// Customized actions for the bot.
|
||||||
type Action interface {
|
type Action interface {
|
||||||
Act(Context)
|
Act(Context)
|
||||||
|
@ -166,29 +157,18 @@ type Action interface {
|
||||||
|
|
||||||
type ActionFunc func(Context)
|
type ActionFunc func(Context)
|
||||||
|
|
||||||
func (af ActionFunc) Act(c *Context) {
|
func (af ActionFunc) Act(c Context) {
|
||||||
af(c)
|
af(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) History() []Path {
|
|
||||||
return c.session.pathHistory
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Context) PathExist(pth Path) bool {
|
|
||||||
return c.bot.behaviour.PathExist(pth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple way to read strings for widgets with
|
// Simple way to read strings for widgets with
|
||||||
// the specified prompt.
|
// the specified prompt.
|
||||||
func (c Context) ReadString(promptf string, args ...any) string {
|
func (c Context) ReadString(promptf string, args ...any) string {
|
||||||
var text string
|
var text string
|
||||||
if pref != "" {
|
if promptf != "" {
|
||||||
c.Sendf(promptf, args...)
|
c.Sendf(promptf, args...)
|
||||||
}
|
}
|
||||||
for u := range c.Input() {
|
for u := range c.Input() {
|
||||||
if u == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if u.Message == nil {
|
if u.Message == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -198,19 +178,25 @@ func (c Context) ReadString(promptf string, args ...any) string {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) Update() Update {
|
func (c Context) Update(updater Updater) error {
|
||||||
return c.update
|
return updater.Update(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) CallbackUpdate() *Update {
|
||||||
|
return &c.update
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the reader for specified file ID and path.
|
// Returns the reader for specified file ID and path.
|
||||||
func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
|
func (c Context) GetFile(fileID FileID) (io.ReadCloser, string, error) {
|
||||||
file, err := c.Bot.Api.GetFile(tgbotapi.FileConfig{FileID:string(fileId)})
|
file, err := c.Bot().API().GetFile(tgbotapi.FileConfig{
|
||||||
|
FileID: string(fileID),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
r, err := http.Get(fmt.Sprintf(
|
r, err := http.Get(fmt.Sprintf(
|
||||||
"https://api.telegram.org/file/bot%s/%s",
|
"https://api.telegram.org/file/bot%s/%s",
|
||||||
c.Bot.Api.Token,
|
c.Bot().API().Token,
|
||||||
file.FilePath,
|
file.FilePath,
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -223,8 +209,9 @@ func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
|
||||||
return r.Body, file.FilePath, nil
|
return r.Body, file.FilePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) ReadFile(fileId FileId) ([]byte, string, error) {
|
// Reads all the content from the specified file.
|
||||||
file, pth, err := c.GetFile(fileId)
|
func (c Context) ReadFile(fileID FileID) ([]byte, string, error) {
|
||||||
|
file, pth, err := c.GetFile(fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
@ -238,3 +225,172 @@ func (c *Context) ReadFile(fileId FileId) ([]byte, string, error) {
|
||||||
return bts, pth, nil
|
return bts, pth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Context) RunCompo(compo Component) (*UpdateChan, error) {
|
||||||
|
if compo == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
sendable, canSend := compo.(Sendable)
|
||||||
|
if canSend {
|
||||||
|
msg, err := c.Send(sendable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sendable.SetMessage(msg)
|
||||||
|
}
|
||||||
|
updates := NewUpdateChan()
|
||||||
|
go func() {
|
||||||
|
compo.Serve(
|
||||||
|
c.WithInput(updates),
|
||||||
|
)
|
||||||
|
// To let widgets finish themselves before
|
||||||
|
// the channel is closed and close it by themselves.
|
||||||
|
updates.Close()
|
||||||
|
}()
|
||||||
|
return updates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run widget in background returning the new input channel for it.
|
||||||
|
func (c Context) RunWidget(widget Widget) (*UpdateChan, error) {
|
||||||
|
var err error
|
||||||
|
if widget == nil {
|
||||||
|
return nil, EmptyWidgetErr
|
||||||
|
}
|
||||||
|
|
||||||
|
compos := widget.Render(c)
|
||||||
|
// Leave if changed path or components are empty.
|
||||||
|
if compos == nil {
|
||||||
|
return nil, EmptyCompoErr
|
||||||
|
}
|
||||||
|
chns := make([]*UpdateChan, len(compos))
|
||||||
|
for i, compo := range compos {
|
||||||
|
chns[i], err = c.RunCompo(compo)
|
||||||
|
if err != nil {
|
||||||
|
for _, chn := range chns {
|
||||||
|
chn.Close()
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := NewUpdateChan()
|
||||||
|
go func() {
|
||||||
|
ln := len(compos)
|
||||||
|
//ation: u != nil (mismatchedtypes Update and untyped nil)
|
||||||
|
UPDATE:
|
||||||
|
for u := range ret.Chan() {
|
||||||
|
cnt := 0
|
||||||
|
for i, compo := range compos {
|
||||||
|
chn := chns[i]
|
||||||
|
if chn.Closed() {
|
||||||
|
cnt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !compo.Filter(u) {
|
||||||
|
chn.Send(u)
|
||||||
|
continue UPDATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cnt == ln {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.Close()
|
||||||
|
for _, chn := range chns {
|
||||||
|
chn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) GoRet(pth Widget) UI {
|
||||||
|
return UI{WidgetGo{
|
||||||
|
Path: pth,
|
||||||
|
Arg: c.Arg(),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the specified widget
|
||||||
|
// using context values.
|
||||||
|
func (c Context) Go(pth Widget) error {
|
||||||
|
var err error
|
||||||
|
if pth == nil {
|
||||||
|
c.session.pathHistory = []Widget{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var back bool
|
||||||
|
if pth == Back {
|
||||||
|
if len(c.session.pathHistory) <= 1 {
|
||||||
|
return c.Go(nil)
|
||||||
|
}
|
||||||
|
pth = c.session.pathHistory[len(c.session.pathHistory)-2]
|
||||||
|
c.session.pathHistory =
|
||||||
|
c.session.pathHistory[:len(c.session.pathHistory)-1]
|
||||||
|
back = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !back {
|
||||||
|
c.session.pathHistory = append(c.session.pathHistory, pth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stopping the current widget.
|
||||||
|
c.session.skippedUpdates.Close()
|
||||||
|
|
||||||
|
// Running the new one.
|
||||||
|
c.session.skippedUpdates, err = c.RunWidget(pth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) Session() Session {
|
||||||
|
return *c.session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) SetSessionData(v any) {
|
||||||
|
c.session.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) SessionData() any {
|
||||||
|
return c.session.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) SessionID() SessionID {
|
||||||
|
return c.session.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) SessionScope() SessionScope {
|
||||||
|
return c.session.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only for the root widget usage.
|
||||||
|
// Skip the update sending it down to
|
||||||
|
// the underlying widget.
|
||||||
|
func (c Context) SkipUpdate(u Update) {
|
||||||
|
c.session.skippedUpdates.Send(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the session related bot.
|
||||||
|
func (c Context) Bot() *Bot {
|
||||||
|
return c.session.bot
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return context's session's path history.
|
||||||
|
func (c Context) PathHistory() []Widget {
|
||||||
|
return c.session.pathHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) SetPathHistory(hist []Widget) {
|
||||||
|
c.session.pathHistory = hist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Context) Path() Widget {
|
||||||
|
ln := len(c.session.pathHistory)
|
||||||
|
if ln == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.session.pathHistory[ln-1]
|
||||||
|
}
|
||||||
|
|
4
devel-loop
Executable file
4
devel-loop
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
wgo sh -c './btest && ./exe/test'
|
||||||
|
|
13
file.go
13
file.go
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
"github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FileID string
|
||||||
type FileConfig = tgbotapi.FileConfig
|
type FileConfig = tgbotapi.FileConfig
|
||||||
type PhotoConfig = tgbotapi.PhotoConfig
|
type PhotoConfig = tgbotapi.PhotoConfig
|
||||||
type FileType int
|
type FileType int
|
||||||
|
@ -40,7 +41,7 @@ type File struct {
|
||||||
func NewFile(reader io.Reader) *File {
|
func NewFile(reader io.Reader) *File {
|
||||||
ret := &File{}
|
ret := &File{}
|
||||||
|
|
||||||
ret.MessageCompo = NewMessage("")
|
ret.MessageCompo = *Messagef("")
|
||||||
ret.reader = reader
|
ret.reader = reader
|
||||||
ret.upload = true
|
ret.upload = true
|
||||||
|
|
||||||
|
@ -107,21 +108,21 @@ func (f *File) SendData() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) SendConfig(
|
func (f *File) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (SendConfig) {
|
) (SendConfig) {
|
||||||
var config SendConfig
|
var config SendConfig
|
||||||
cid := sid.ToApi()
|
cid := sid.ToAPI()
|
||||||
|
|
||||||
switch f.Type() {
|
switch f.Type() {
|
||||||
case PhotoFileType:
|
case PhotoFileType:
|
||||||
photo := tgbotapi.NewPhoto(cid, f)
|
photo := tgbotapi.NewPhoto(cid, f)
|
||||||
photo.Caption = f.caption
|
photo.Caption = f.caption
|
||||||
|
|
||||||
config.Photo = &photo
|
config.Chattable = photo
|
||||||
case DocumentFileType:
|
case DocumentFileType:
|
||||||
doc := tgbotapi.NewDocument(sid.ToApi(), f)
|
doc := tgbotapi.NewDocument(sid.ToAPI(), f)
|
||||||
doc.Caption = f.caption
|
doc.Caption = f.caption
|
||||||
config.Document = &doc
|
config.Chattable = doc
|
||||||
default:
|
default:
|
||||||
panic(UnknownFileTypeErr)
|
panic(UnknownFileTypeErr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,12 @@ package tg
|
||||||
type Filterer interface {
|
type Filterer interface {
|
||||||
// Return true if should filter the update
|
// Return true if should filter the update
|
||||||
// and not send it inside the widget.
|
// and not send it inside the widget.
|
||||||
Filter(*Update) bool
|
Filter(Update) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterFunc func(*Update) bool
|
type FilterFunc func(Update) bool
|
||||||
func (f FilterFunc) Filter(
|
func (f FilterFunc) Filter(
|
||||||
u *Update,
|
u Update,
|
||||||
) bool {
|
) bool {
|
||||||
return f(u)
|
return f(u)
|
||||||
}
|
}
|
||||||
|
|
29
go.go
29
go.go
|
@ -1,22 +1,33 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
func Go(pth Path) UI {
|
func Go(pth Widget) UI {
|
||||||
return UI{
|
return UI{
|
||||||
GoWidget(pth),
|
WidgetGo{
|
||||||
|
Path: pth,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GoWidget string
|
// The type implements changing current path to the widget.
|
||||||
// Implementing the Server interface.
|
type WidgetGo struct {
|
||||||
func (widget GoWidget) Serve(c Context) {
|
Path Widget
|
||||||
c.input.Close()
|
Arg any
|
||||||
c.Go(Path(widget))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget GoWidget) Render(c Context) UI {
|
func (w WidgetGo) Act(c Context) {
|
||||||
|
c.WithArg(w.Arg).Go(w.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementing the Server interface.
|
||||||
|
func (widget WidgetGo) Serve(c Context) {
|
||||||
|
c.input.Close()
|
||||||
|
c.WithArg(widget.Path).Go(widget.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget WidgetGo) Render(c Context) UI {
|
||||||
return UI{widget}
|
return UI{widget}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget GoWidget) Filter(u Update) bool {
|
func (widget WidgetGo) Filter(u Update) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,4 +1,4 @@
|
||||||
module vultras.su/core/tg
|
module surdeus.su/core/tg
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
|
|
46
inline.go
46
inline.go
|
@ -11,7 +11,7 @@ type Inline struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the inline keyboard to markup for the tgbotapi.
|
// Convert the inline keyboard to markup for the tgbotapi.
|
||||||
func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
|
func (kbd Inline) ToAPI() tgbotapi.InlineKeyboardMarkup {
|
||||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||||
for _, row := range kbd.Rows {
|
for _, row := range kbd.Rows {
|
||||||
if row == nil {
|
if row == nil {
|
||||||
|
@ -19,7 +19,7 @@ func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
|
||||||
}
|
}
|
||||||
buttons := []tgbotapi.InlineKeyboardButton{}
|
buttons := []tgbotapi.InlineKeyboardButton{}
|
||||||
for _, button := range row {
|
for _, button := range row {
|
||||||
if button == nil {
|
if !button.Valid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buttons = append(buttons, button.ToTelegramInline())
|
buttons = append(buttons, button.ToTelegramInline())
|
||||||
|
@ -38,12 +38,14 @@ type InlineCompo struct {
|
||||||
|
|
||||||
// Implementing the Sendable interface.
|
// Implementing the Sendable interface.
|
||||||
func (compo *InlineCompo) SendConfig(
|
func (compo *InlineCompo) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (SendConfig) {
|
) (SendConfig) {
|
||||||
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
|
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
|
||||||
|
msg := sendConfig.Chattable.(tgbotapi.MessageConfig)
|
||||||
if len(compo.Inline.Rows) > 0 {
|
if len(compo.Inline.Rows) > 0 {
|
||||||
sendConfig.Message.ReplyMarkup = compo.Inline.ToApi()
|
msg.ReplyMarkup = compo.Inline.ToAPI()
|
||||||
}
|
}
|
||||||
|
sendConfig.Chattable = msg
|
||||||
|
|
||||||
return sendConfig
|
return sendConfig
|
||||||
}
|
}
|
||||||
|
@ -51,34 +53,39 @@ func (compo *InlineCompo) SendConfig(
|
||||||
// Update the component on the client side.
|
// Update the component on the client side.
|
||||||
// Requires exactly the pointer but not the value
|
// Requires exactly the pointer but not the value
|
||||||
// cause it changes insides of the structure.
|
// cause it changes insides of the structure.
|
||||||
func (compo *InlineCompo) Update(c Context) {
|
func (compo *InlineCompo) Update(c Context) error {
|
||||||
if compo.Message != nil {
|
if compo.Message != nil {
|
||||||
var edit tgbotapi.Chattable
|
var edit tgbotapi.Chattable
|
||||||
markup := compo.Inline.ToApi()
|
markup := compo.Inline.ToAPI()
|
||||||
ln := len(markup.InlineKeyboard)
|
ln := len(markup.InlineKeyboard)
|
||||||
if ln == 0 || compo.Inline.Rows == nil {
|
if ln == 0 || compo.Inline.Rows == nil {
|
||||||
edit = tgbotapi.NewEditMessageText(
|
edit = tgbotapi.NewEditMessageText(
|
||||||
c.Session.Id.ToApi(),
|
c.SessionID().ToAPI(),
|
||||||
compo.Message.MessageID,
|
compo.Message.MessageID,
|
||||||
compo.Text,
|
compo.Text,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
edit = tgbotapi.NewEditMessageTextAndMarkup(
|
edit = tgbotapi.NewEditMessageTextAndMarkup(
|
||||||
c.Session.Id.ToApi(),
|
c.SessionID().ToAPI(),
|
||||||
compo.Message.MessageID,
|
compo.Message.MessageID,
|
||||||
compo.Text,
|
compo.Text,
|
||||||
markup,
|
markup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
msg, _ := c.Bot.Api.Send(edit)
|
msg, err := c.Bot().API().Send(edit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
compo.Message = &msg
|
compo.Message = &msg
|
||||||
}
|
}
|
||||||
compo.buttonMap = compo.MakeButtonMap()
|
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the Filterer interface.
|
// Implementing the Filterer interface.
|
||||||
func (compo InlineCompo) Filter(u Update) bool {
|
func (compo *InlineCompo) Filter(u Update) bool {
|
||||||
if compo == nil || u.CallbackQuery == nil {
|
if u.CallbackQuery == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,13 +98,13 @@ func (compo InlineCompo) Filter(u Update) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the Server interface.
|
// Implementing the Server interface.
|
||||||
func (compo InlineCompo) Serve(c Context) {
|
func (compo *InlineCompo) Serve(c Context) {
|
||||||
for u := range c.Input() {
|
for u := range c.Input() {
|
||||||
compo.OnOneUpdate(c, u)
|
compo.OnOneUpdate(c, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
|
func (compo *InlineCompo) OnOneUpdate(c Context, u Update) error {
|
||||||
var act Action
|
var act Action
|
||||||
btns := compo.ButtonMap()
|
btns := compo.ButtonMap()
|
||||||
cb := tgbotapi.NewCallback(
|
cb := tgbotapi.NewCallback(
|
||||||
|
@ -106,20 +113,23 @@ func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
|
||||||
)
|
)
|
||||||
data := u.CallbackQuery.Data
|
data := u.CallbackQuery.Data
|
||||||
|
|
||||||
_, err := c.Bot.Api.Request(cb)
|
_, err := c.Bot().API().Request(cb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
btn, ok := btns[data]
|
btn, ok := btns[data]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if btn != nil {
|
|
||||||
|
if btn.Action != nil {
|
||||||
act = btn.Action
|
act = btn.Action
|
||||||
} else if compo.Action != nil {
|
} else if compo.Action != nil {
|
||||||
act = compo.Action
|
act = compo.Action
|
||||||
}
|
}
|
||||||
c.WithUpdate(u).Run(act)
|
c.WithUpdate(u).Run(act)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ type InvoiceCompo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (compo *InvoiceCompo) SendConfig(
|
func (compo *InvoiceCompo) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (*SendConfig) {
|
) (*SendConfig) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
19
keyboard.go
19
keyboard.go
|
@ -10,12 +10,11 @@ type Keyboard struct {
|
||||||
// defined action for the button.
|
// defined action for the button.
|
||||||
Action Action
|
Action Action
|
||||||
Rows []ButtonRow
|
Rows []ButtonRow
|
||||||
buttonMap ButtonMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the new keyboard with specified rows.
|
// Returns the new keyboard with specified rows.
|
||||||
func NewKeyboard(rows ...ButtonRow) Keyboard {
|
func NewKeyboard(rows ...ButtonRow) Keyboard {
|
||||||
ret := &Keyboard{}
|
ret := Keyboard{}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if row != nil && len(row) > 0 {
|
if row != nil && len(row) > 0 {
|
||||||
ret.Rows = append(ret.Rows, row)
|
ret.Rows = append(ret.Rows, row)
|
||||||
|
@ -41,9 +40,10 @@ func (kbd Keyboard) Row(btns ...Button) Keyboard {
|
||||||
if len(btns) < 1 {
|
if len(btns) < 1 {
|
||||||
return kbd
|
return kbd
|
||||||
}
|
}
|
||||||
retBtns := []*Button{}
|
|
||||||
|
retBtns := make([]Button, 0, len(btns))
|
||||||
for _, btn := range btns {
|
for _, btn := range btns {
|
||||||
if btn == nil {
|
if !btn.Valid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
retBtns = append(retBtns, btn)
|
retBtns = append(retBtns, btn)
|
||||||
|
@ -58,7 +58,7 @@ func (kbd Keyboard) Row(btns ...Button) Keyboard {
|
||||||
// Adds buttons as one column list.
|
// Adds buttons as one column list.
|
||||||
func (kbd Keyboard) List(btns ...Button) Keyboard {
|
func (kbd Keyboard) List(btns ...Button) Keyboard {
|
||||||
for _, btn := range btns {
|
for _, btn := range btns {
|
||||||
if btn == nil {
|
if !btn.Valid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kbd.Rows = append(kbd.Rows, ButtonRow{btn})
|
kbd.Rows = append(kbd.Rows, ButtonRow{btn})
|
||||||
|
@ -76,10 +76,7 @@ func (kbd Keyboard) WithAction(a Action) Keyboard {
|
||||||
// Returns the map of buttons. Where the key
|
// Returns the map of buttons. Where the key
|
||||||
// is button data and the value is Action.
|
// is button data and the value is Action.
|
||||||
func (kbd Keyboard) ButtonMap() ButtonMap {
|
func (kbd Keyboard) ButtonMap() ButtonMap {
|
||||||
if kbd.buttonMap == nil {
|
return kbd.MakeButtonMap()
|
||||||
kbd.buttonMap = kbd.MakeButtonMap()
|
|
||||||
}
|
|
||||||
return kbd.buttonMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the map of buttons on the most fresh version of the keyboard.
|
// Returns the map of buttons on the most fresh version of the keyboard.
|
||||||
|
@ -90,7 +87,6 @@ func (kbd Keyboard) MakeButtonMap() ButtonMap {
|
||||||
ret[vj.Key()] = vj
|
ret[vj.Key()] = vj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kbd.buttonMap = ret
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
@ -103,8 +99,9 @@ func (kbd Keyboard) Inline() Inline {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the keyboard to the more specific reply one.
|
// Convert the keyboard to the more specific reply one.
|
||||||
|
// By default OneTime = true.
|
||||||
func (kbd Keyboard) Reply() Reply {
|
func (kbd Keyboard) Reply() Reply {
|
||||||
ret := &Reply{}
|
ret := Reply{}
|
||||||
ret.Keyboard = kbd
|
ret.Keyboard = kbd
|
||||||
// it is used more often than not once.
|
// it is used more often than not once.
|
||||||
ret.OneTime = true
|
ret.OneTime = true
|
||||||
|
|
16
location.go
16
location.go
|
@ -7,22 +7,22 @@ import (
|
||||||
type Location = tgbotapi.Location
|
type Location = tgbotapi.Location
|
||||||
|
|
||||||
type LocationCompo struct {
|
type LocationCompo struct {
|
||||||
*MessageCompo
|
MessageCompo
|
||||||
Location
|
Location
|
||||||
}
|
}
|
||||||
|
|
||||||
func (compo *LocationCompo) SendConfig(
|
func (compo *LocationCompo) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (*SendConfig) {
|
) (SendConfig) {
|
||||||
cid := sid.ToApi()
|
cid := sid.ToAPI()
|
||||||
loc := tgbotapi.NewLocation(
|
location := tgbotapi.NewLocation(
|
||||||
cid,
|
cid,
|
||||||
compo.Latitude,
|
compo.Latitude,
|
||||||
compo.Longitude,
|
compo.Longitude,
|
||||||
)
|
)
|
||||||
ret := &SendConfig{
|
|
||||||
Location: &loc,
|
ret := SendConfig{}
|
||||||
}
|
ret.Chattable = location
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
109
message.go
109
message.go
|
@ -10,8 +10,13 @@ type Message = tgbotapi.Message
|
||||||
|
|
||||||
// Simple text message component type.
|
// Simple text message component type.
|
||||||
type MessageCompo struct {
|
type MessageCompo struct {
|
||||||
Message Message
|
// Low level Message represents
|
||||||
|
// the already sent to the client message.
|
||||||
|
// Will be nil if the message is not rendered to the client.
|
||||||
|
Message *Message
|
||||||
|
// Parsing mode for the text: HTML, MD, MD2...
|
||||||
ParseMode string
|
ParseMode string
|
||||||
|
// The text to display.
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,111 +34,121 @@ func Escape2(str string) string {
|
||||||
// Call the function after the message was sent.
|
// Call the function after the message was sent.
|
||||||
func (compo *MessageCompo) Update(c Context) error {
|
func (compo *MessageCompo) Update(c Context) error {
|
||||||
edit := tgbotapi.NewEditMessageText(
|
edit := tgbotapi.NewEditMessageText(
|
||||||
c.Session.Id.ToApi(),
|
c.Session().ID.ToAPI(),
|
||||||
compo.Message.MessageID,
|
compo.Message.MessageID,
|
||||||
compo.Text,
|
compo.Text,
|
||||||
)
|
)
|
||||||
msg, err := c.bot.api.Send(edit)
|
msg, err := c.Bot().API().Send(edit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
compo.Message = msg
|
compo.Message = &msg
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (compo *MessageCompo) Delete(c *Context) {
|
// Calling the method removes the message on the client side
|
||||||
cfg := tgbotapi.NewDeleteMessage(c.Session.Id.ToApi(), compo.Message.MessageID)
|
// and sets the Message in the component to nil.
|
||||||
c.Bot.Api.Send(cfg)
|
func (compo *MessageCompo) Delete(c Context) error {
|
||||||
//c.Sendf("%q", err)
|
cfg := tgbotapi.NewDeleteMessage(c.session.ID.ToAPI(), compo.Message.MessageID)
|
||||||
|
_, err := c.Bot().API().Send(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty the message if success.
|
||||||
|
compo.Message = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is only implemented to make it sendable and so we can put it
|
// Return new message with the specified text
|
||||||
// return of rendering functions.
|
// formatted with the fmt.Sprintf function.
|
||||||
func (compo *MessageCompo) SetMessage(msg Message) {
|
func Messagef(format string, v ...any) *MessageCompo {
|
||||||
compo.Message = msg
|
ret := &MessageCompo{}
|
||||||
}
|
|
||||||
|
|
||||||
// Return new message with the specified text.
|
|
||||||
func Messagef(format string, v ...any) MessageCompo {
|
|
||||||
ret := MessageCompo{}
|
|
||||||
ret.Text = fmt.Sprintf(format, v...)
|
ret.Text = fmt.Sprintf(format, v...)
|
||||||
ret.ParseMode = tgbotapi.ModeMarkdown
|
ret.ParseMode = tgbotapi.ModeMarkdown
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return message with the specified parse mode.
|
// Return message with the specified parse mode.
|
||||||
func (msg MessageCompo) withParseMode(mode string) MessageCompo {
|
func (compo *MessageCompo) setParseMode(mode string) *MessageCompo {
|
||||||
msg.ParseMode = mode
|
compo.ParseMode = mode
|
||||||
return msg
|
return compo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the default Markdown parsing mode.
|
// Set the default Markdown parsing mode.
|
||||||
func (msg MessageCompo) MD() MessageCompo {
|
func (compo *MessageCompo) MD() *MessageCompo {
|
||||||
return msg.withParseMode(tgbotapi.ModeMarkdown)
|
return compo.setParseMode(tgbotapi.ModeMarkdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the Markdown 2 parsing mode.
|
// Set the Markdown 2 parsing mode.
|
||||||
func (msg MessageCompo) MD2() MessageCompo {
|
func (compo *MessageCompo) MD2() *MessageCompo {
|
||||||
return msg.withParseMode(tgbotapi.ModeMarkdownV2)
|
return compo.setParseMode(tgbotapi.ModeMarkdownV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the HTML parsing mode.
|
// Set the HTML parsing mode.
|
||||||
func (msg MessageCompo) HTML() MessageCompo {
|
func (compo *MessageCompo) HTML() *MessageCompo {
|
||||||
return msg.withParseMode(tgbotapi.ModeHTML)
|
return compo.setParseMode(tgbotapi.ModeHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform the message component into one with reply keyboard.
|
// Transform the message component into one with reply keyboard.
|
||||||
func (msg MessageCompo) Inline(inline Inline) InlineCompo {
|
func (compo *MessageCompo) Inline(inline Inline) *InlineCompo {
|
||||||
return InlineCompo{
|
return &InlineCompo{
|
||||||
Inline: inline,
|
Inline: inline,
|
||||||
MessageCompo: msg,
|
MessageCompo: *compo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform the message component into one with reply keyboard.
|
// Transform the message component into one with reply keyboard.
|
||||||
func (msg MessageCompo) Reply(reply Reply) ReplyCompo {
|
func (msg *MessageCompo) Reply(reply Reply) *ReplyCompo {
|
||||||
return ReplyCompo{
|
return &ReplyCompo{
|
||||||
Reply: reply,
|
Reply: reply,
|
||||||
MessageCompo: msg,
|
MessageCompo: *msg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform the message component into the location one.
|
// Transform the message component into the location one.
|
||||||
func (msg MessageCompo) Location(
|
func (msg *MessageCompo) Location(
|
||||||
lat, long float64,
|
lat, long float64,
|
||||||
) LocationCompo {
|
) *LocationCompo {
|
||||||
ret := &LocationCompo{
|
ret := &LocationCompo{}
|
||||||
MessageCompo: msg,
|
ret.MessageCompo = *msg
|
||||||
Location: Location{
|
ret.Latitude = lat
|
||||||
Latitude: lat,
|
ret.Longitude = long
|
||||||
Longitude: long,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the Sendable interface.
|
// Implementing the Sendable interface.
|
||||||
func (config MessageCompo) SendConfig(
|
func (compo *MessageCompo) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (SendConfig) {
|
) (SendConfig) {
|
||||||
var (
|
var (
|
||||||
ret SendConfig
|
ret SendConfig
|
||||||
text string
|
text string
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.Text == "" {
|
// Protection against empty text,
|
||||||
|
// since it breaks the Telegram bot API.
|
||||||
|
if compo.Text == "" {
|
||||||
text = ">"
|
text = ">"
|
||||||
} else {
|
} else {
|
||||||
text = config.Text
|
text = compo.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(sid.ToApi(), text)
|
msg := tgbotapi.NewMessage(sid.ToAPI(), text)
|
||||||
msg.ParseMode = config.ParseMode
|
msg.ParseMode = compo.ParseMode
|
||||||
ret.Chattable = msg
|
ret.Chattable = msg
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementing the Sendable interface.
|
||||||
|
// Also used for embedding for things like InlineCompo etc.
|
||||||
|
func (compo *MessageCompo) SetMessage(msg *Message) {
|
||||||
|
compo.Message = msg
|
||||||
|
}
|
||||||
|
|
||||||
// Empty serving to use messages in rendering.
|
// Empty serving to use messages in rendering.
|
||||||
func (compo *MessageCompo) Serve(c Context) {}
|
func (compo *MessageCompo) Serve(c Context) {}
|
||||||
|
|
||||||
|
|
113
paged-panel.go
Normal file
113
paged-panel.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package tg
|
||||||
|
|
||||||
|
type PanelPager interface {
|
||||||
|
GetPanelPage(
|
||||||
|
panel *PanelPagerCompo,
|
||||||
|
c Context, page, size int,
|
||||||
|
) PanelPage
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelPage struct {
|
||||||
|
Next, Prev bool
|
||||||
|
Rows []ButtonRow
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelPagerFunc func(
|
||||||
|
panel *PanelPagerCompo,
|
||||||
|
c Context, page, size int,
|
||||||
|
) PanelPage
|
||||||
|
func (fn PanelPagerFunc) GetPanelPage(
|
||||||
|
panel *PanelPagerCompo, c Context, page, size int,
|
||||||
|
) PanelPage {
|
||||||
|
return fn(panel, c, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelPagerCompo struct {
|
||||||
|
PanelCompo
|
||||||
|
page int
|
||||||
|
size int
|
||||||
|
nextFormat, prevFormat, delFormat string
|
||||||
|
pager PanelPager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (compo *MessageCompo) PanelPager(
|
||||||
|
c Context,
|
||||||
|
startPage, size int,
|
||||||
|
pager PanelPager,
|
||||||
|
) (*PanelPagerCompo) {
|
||||||
|
ret := &PanelPagerCompo{}
|
||||||
|
ret.page = startPage
|
||||||
|
ret.size = size
|
||||||
|
ret.pager = pager
|
||||||
|
ret.prevFormat = "<<<"
|
||||||
|
ret.nextFormat = ">>>"
|
||||||
|
ret.delFormat = "..."
|
||||||
|
|
||||||
|
ret.PanelCompo = (*compo.Panel(
|
||||||
|
c, ret,
|
||||||
|
))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (panel *PanelPagerCompo) GetPanelRows(
|
||||||
|
pcompo *PanelCompo, c Context,
|
||||||
|
) []ButtonRow {
|
||||||
|
page := panel.pager.GetPanelPage(
|
||||||
|
panel, c, panel.page, panel.size,
|
||||||
|
)
|
||||||
|
|
||||||
|
controlRow := ButtonRow{}
|
||||||
|
|
||||||
|
rows := page.Rows
|
||||||
|
next := func(c Context){
|
||||||
|
panel.page++
|
||||||
|
panel.Update(c)
|
||||||
|
}
|
||||||
|
prev := func(c Context){
|
||||||
|
panel.page--
|
||||||
|
panel.Update(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.Prev {
|
||||||
|
if panel.delFormat != "" {
|
||||||
|
rows = append(
|
||||||
|
[]ButtonRow{
|
||||||
|
ButtonRow{
|
||||||
|
Buttonf(panel.delFormat).Rand().
|
||||||
|
WithAction(Func(prev)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rows...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
controlRow = append(
|
||||||
|
controlRow,
|
||||||
|
Buttonf(panel.prevFormat).Rand().
|
||||||
|
WithAction(Func(prev)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.Next {
|
||||||
|
if panel.delFormat != "" {
|
||||||
|
rows = append(
|
||||||
|
rows,
|
||||||
|
ButtonRow{
|
||||||
|
Buttonf(panel.delFormat).Rand().
|
||||||
|
WithAction(Func(next)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
controlRow = append(
|
||||||
|
controlRow,
|
||||||
|
Buttonf(panel.nextFormat).Rand().
|
||||||
|
WithAction(Func(next)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(
|
||||||
|
rows,
|
||||||
|
controlRow,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
43
panel.go
43
panel.go
|
@ -1,40 +1,55 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
|
// Using the interface and all related is
|
||||||
|
// deprecated. Use the Paneler interface and function.
|
||||||
type Rowser interface {
|
type Rowser interface {
|
||||||
MakeRows(c *Context) []ButtonRow
|
MakeRows(c Context) []ButtonRow
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowserFunc func(c *Context) []ButtonRow
|
type RowserFunc func(c Context) []ButtonRow
|
||||||
func (fn RowserFunc) MakeRows(c *Context) []ButtonRow {
|
func (fn RowserFunc) MakeRows(c Context) []ButtonRow {
|
||||||
return fn(c)
|
return fn(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Paneler interface {
|
||||||
|
GetPanelRows(*PanelCompo, Context) []ButtonRow
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelFunc func(*PanelCompo, Context) []ButtonRow
|
||||||
|
func (fn PanelFunc) GetPanelRows(
|
||||||
|
panel *PanelCompo, c Context,
|
||||||
|
) []ButtonRow {
|
||||||
|
return fn(panel, c)
|
||||||
|
}
|
||||||
|
|
||||||
// The type represents the inline panel with
|
// The type represents the inline panel with
|
||||||
// scrollable via buttons content.
|
// scrollable via buttons content.
|
||||||
// Can be used for example to show users via SQL and offset
|
// Can be used for example to show users via SQL and offset
|
||||||
// or something like that.
|
// or something like that.
|
||||||
type PanelCompo struct {
|
type PanelCompo struct {
|
||||||
*InlineCompo
|
InlineCompo
|
||||||
Rowser Rowser
|
Paneler Paneler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform to the panel with dynamic rows.
|
// Transform to the panel with dynamic rows.
|
||||||
func (compo *MessageCompo) Panel(
|
func (compo *MessageCompo) Panel(
|
||||||
c *Context, // The context that all the buttons will get.
|
c Context, // The context to generate the first page of buttons.
|
||||||
rowser Rowser, // The rows generator.
|
paneler Paneler, // The rows generator.
|
||||||
) *PanelCompo {
|
) *PanelCompo {
|
||||||
ret := &PanelCompo{}
|
ret := &PanelCompo{}
|
||||||
ret.InlineCompo = compo.Inline(
|
ret.Paneler = paneler
|
||||||
|
|
||||||
|
ret.InlineCompo = (*compo.Inline(
|
||||||
NewKeyboard(
|
NewKeyboard(
|
||||||
rowser.MakeRows(c)...,
|
ret.Paneler.GetPanelRows(ret, c)...,
|
||||||
).Inline(),
|
).Inline(),
|
||||||
)
|
))
|
||||||
ret.Rowser = rowser
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (compo *PanelCompo) Update(c *Context) {
|
// Implementing the Updater.
|
||||||
compo.Rows = compo.Rowser.MakeRows(c)
|
func (panel *PanelCompo) Update(c Context) error {
|
||||||
compo.InlineCompo.Update(c)
|
panel.Rows = panel.Paneler.GetPanelRows(panel, c)
|
||||||
|
return panel.InlineCompo.Update(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
reply.go
35
reply.go
|
@ -41,7 +41,7 @@ func (kbd Reply) ToApi() any {
|
||||||
}
|
}
|
||||||
buttons := []tgbotapi.KeyboardButton{}
|
buttons := []tgbotapi.KeyboardButton{}
|
||||||
for _, button := range row {
|
for _, button := range row {
|
||||||
if button == nil {
|
if !button.Valid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buttons = append(buttons, button.ToTelegram())
|
buttons = append(buttons, button.ToTelegram())
|
||||||
|
@ -63,17 +63,21 @@ type ReplyCompo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the sendable interface.
|
// Implementing the sendable interface.
|
||||||
func (compo ReplyCompo) SendConfig(
|
func (compo *ReplyCompo) SendConfig(
|
||||||
sid SessionId, bot *Bot,
|
sid SessionID, bot *Bot,
|
||||||
) (SendConfig) {
|
) (SendConfig) {
|
||||||
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
|
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
|
||||||
sendConfig.Message.ReplyMarkup = compo.Reply.ToApi()
|
|
||||||
|
msg := sendConfig.Chattable.(tgbotapi.MessageConfig)
|
||||||
|
msg.ReplyMarkup = compo.Reply.ToApi()
|
||||||
|
sendConfig.Chattable = msg
|
||||||
|
|
||||||
return sendConfig
|
return sendConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the Server interface.
|
// Implementing the Server interface.
|
||||||
func (compo ReplyCompo) Filter(
|
func (compo *ReplyCompo) Filter(
|
||||||
u *Update,
|
u Update,
|
||||||
) bool {
|
) bool {
|
||||||
if compo == nil || u.Message == nil {
|
if compo == nil || u.Message == nil {
|
||||||
return true
|
return true
|
||||||
|
@ -82,8 +86,8 @@ func (compo ReplyCompo) Filter(
|
||||||
_, ok := compo.ButtonMap()[u.Message.Text]
|
_, ok := compo.ButtonMap()[u.Message.Text]
|
||||||
if !ok {
|
if !ok {
|
||||||
if u.Message.Location != nil {
|
if u.Message.Location != nil {
|
||||||
locBtn := compo.ButtonMap().LocationButton()
|
_, hasLocBtn := compo.ButtonMap().LocationButton()
|
||||||
if locBtn == nil {
|
if !hasLocBtn {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,22 +98,27 @@ func (compo ReplyCompo) Filter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementing the UI interface.
|
// Implementing the UI interface.
|
||||||
func (compo ReplyCompo) Serve(c *Context) {
|
func (compo *ReplyCompo) Serve(c Context) {
|
||||||
for u := range c.Input() {
|
for u := range c.Input() {
|
||||||
var btn *Button
|
var btn Button
|
||||||
text := u.Message.Text
|
text := u.Message.Text
|
||||||
btns := compo.ButtonMap()
|
btns := compo.ButtonMap()
|
||||||
|
|
||||||
btn, ok := btns[text]
|
btn, ok := btns[text]
|
||||||
if !ok {
|
if !ok {
|
||||||
if u.Message.Location != nil {
|
if u.Message.Location != nil {
|
||||||
btn = btns.LocationButton()
|
locBtn, hasLocBtn := btns.LocationButton()
|
||||||
|
if hasLocBtn {
|
||||||
|
btn = locBtn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if btn != nil {
|
if !btn.Valid {
|
||||||
c.WithUpdate(u).Run(btn.Action)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.WithUpdate(u).Run(btn.Action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
66
screen.go
66
screen.go
|
@ -1,48 +1,32 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
import (
|
type WidgetSpecial int
|
||||||
"path"
|
const (
|
||||||
|
widgetEmpty WidgetSpecial = iota
|
||||||
|
widgetBack
|
||||||
)
|
)
|
||||||
|
|
||||||
// The type implements changing screen to the underlying ScreenId
|
|
||||||
type ScreenGo struct {
|
func (w WidgetSpecial) Render(_ Context) UI {
|
||||||
Path Path
|
return nil
|
||||||
Args []any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc ScreenGo) Act(c *Context) {
|
var (
|
||||||
c.Go(sc.Path, sc.Args...)
|
Back = Widget(widgetBack)
|
||||||
}
|
)
|
||||||
|
|
||||||
// The same as Act.
|
/*// Unique identifier for the screen.
|
||||||
func (sc ScreenGo) Serve(c *Context) {
|
type Path int
|
||||||
sc.Act(c)
|
const (
|
||||||
}
|
PathEmpty Path = 0
|
||||||
|
// Going to the path returns
|
||||||
// Unique identifier for the screen
|
// a context to the previous screen.
|
||||||
// and relative paths to the screen.
|
PathBack Path = -1
|
||||||
type Path string
|
)
|
||||||
|
|
||||||
// Returns true if the path is empty.
|
// Returns true if the path is empty.
|
||||||
func (p Path) IsEmpty() bool {
|
func (p Path) IsEmpty() bool {
|
||||||
return p == ""
|
return p == 0
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the path is absolute.
|
|
||||||
func (p Path) IsAbs() bool {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return p[0] == '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Path) Dir() Path {
|
|
||||||
return Path(path.Dir(string(p)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the path deleting exceed ., .. and / .
|
|
||||||
func (p Path) Clean() Path {
|
|
||||||
return Path(path.Clean(string(p)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screen statement of the bot.
|
// Screen statement of the bot.
|
||||||
|
@ -64,10 +48,10 @@ type Node struct {
|
||||||
Path Path
|
Path Path
|
||||||
Screen *Screen
|
Screen *Screen
|
||||||
Subs []*Node
|
Subs []*Node
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Return new root node with the specified widget in the screen.
|
// Return new root node with the specified widget in the screen.
|
||||||
func NewRootNode(widget Widget, subs ...*Node) *RootNode {
|
/*func NewRootNode(widget Widget, subs ...*Node) *RootNode {
|
||||||
ret := &RootNode{}
|
ret := &RootNode{}
|
||||||
ret.Screen = NewScreen(widget)
|
ret.Screen = NewScreen(widget)
|
||||||
ret.Subs = subs
|
ret.Subs = subs
|
||||||
|
@ -114,15 +98,13 @@ func (n *Node) ScreenMap(root Path) ScreenMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Map structure for the screens.
|
|
||||||
type ScreenMap map[Path] *Screen
|
|
||||||
|
|
||||||
// Returns the new screen with specified name and widget.
|
// Returns the new screen with specified name and widget.
|
||||||
func NewScreen(widget Widget) *Screen {
|
/*func NewScreen(widget Widget) *Screen {
|
||||||
return &Screen{
|
return &Screen{
|
||||||
Widget: widget,
|
Widget: widget,
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
6
send.go
6
send.go
|
@ -10,8 +10,8 @@ type MessageId int64
|
||||||
// way to define what message will be
|
// way to define what message will be
|
||||||
// sent to the side of a user.
|
// sent to the side of a user.
|
||||||
type Sendable interface {
|
type Sendable interface {
|
||||||
SendConfig(SessionId, *Bot) (SendConfig)
|
SendConfig(SessionID, *Bot) (SendConfig)
|
||||||
SetMessage(Message)
|
SetMessage(*Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type is used as an endpoint to send messages
|
// The type is used as an endpoint to send messages
|
||||||
|
@ -25,7 +25,7 @@ type SendConfig struct {
|
||||||
type MessageMap map[string] *Message
|
type MessageMap map[string] *Message
|
||||||
|
|
||||||
// Convert to the bot.Api.Send format.
|
// Convert to the bot.Api.Send format.
|
||||||
func (config SendConfig) ToApi() tgbotapi.Chattable {
|
func (config SendConfig) ToAPI() tgbotapi.Chattable {
|
||||||
return config.Chattable
|
return config.Chattable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,6 @@ package tg
|
||||||
// Implementing the interface provides
|
// Implementing the interface provides
|
||||||
// the way to define how to handle updates.
|
// the way to define how to handle updates.
|
||||||
type Server interface {
|
type Server interface {
|
||||||
Serve(*Context)
|
Serve(Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
162
session.go
162
session.go
|
@ -2,11 +2,15 @@ package tg
|
||||||
|
|
||||||
// The type represents map of sessions using
|
// The type represents map of sessions using
|
||||||
// as key.
|
// as key.
|
||||||
type SessionMap map[SessionId]*Session
|
type SessionMap map[SessionID]*Session
|
||||||
|
|
||||||
// Add new empty session by it's ID.
|
// Add new empty session by it's ID.
|
||||||
func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session {
|
func (sm SessionMap) Add(
|
||||||
ret := NewSession(sid, scope)
|
bot *Bot,
|
||||||
|
sid SessionID,
|
||||||
|
scope SessionScope,
|
||||||
|
) *Session {
|
||||||
|
ret := NewSession(bot, sid, scope)
|
||||||
sm[sid] = ret
|
sm[sid] = ret
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
@ -23,159 +27,37 @@ const (
|
||||||
|
|
||||||
// Represents unique value to identify chats.
|
// Represents unique value to identify chats.
|
||||||
// In fact is simply ID of the chat.
|
// In fact is simply ID of the chat.
|
||||||
type SessionId int64
|
type SessionID int64
|
||||||
|
|
||||||
// Convert the SessionId to Telegram API's type.
|
// Convert the SessionID to Telegram API's type.
|
||||||
func (si SessionId) ToApi() int64 {
|
func (si SessionID) ToAPI() int64 {
|
||||||
return int64(si)
|
return int64(si)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type represents current state of
|
// The type represents current state of
|
||||||
// user interaction per each of them.
|
// user interaction per each of them.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
// Id of the chat of the user.
|
// ID of the chat of the user.
|
||||||
Id SessionId
|
ID SessionID
|
||||||
Scope SessionScope
|
Scope SessionScope
|
||||||
// Custom value for each user.
|
// Custom value for each user.
|
||||||
Data any
|
Data any
|
||||||
|
|
||||||
bot *Bot
|
bot *Bot
|
||||||
pathHistory []Path
|
pathHistory []Widget
|
||||||
skippedUpdates *UpdateChan
|
skippedUpdates *UpdateChan
|
||||||
updates *UpdateChan
|
updates *UpdateChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return new empty session with specified user ID.
|
// Return new empty session.
|
||||||
func NewSession(id SessionId, scope SessionScope) *Session {
|
func NewSession(bot *Bot, id SessionID, scope SessionScope) *Session {
|
||||||
return &Session{
|
ret := &Session{}
|
||||||
Id: id,
|
ret.ID = id
|
||||||
Scope: scope,
|
ret.Scope = scope
|
||||||
}
|
ret.bot = bot
|
||||||
|
ret.updates = NewUpdateChan()
|
||||||
|
ret.skippedUpdates = NewUpdateChan()
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes screen of user to the Id one.
|
|
||||||
func (s *Session) go_(pth Path, arg any) error {
|
|
||||||
var err error
|
|
||||||
if pth == "" {
|
|
||||||
s.pathHistory = []Path{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var back bool
|
|
||||||
if pth == "-" {
|
|
||||||
if len(s.pathHistory) < 2 {
|
|
||||||
return s.Go("")
|
|
||||||
}
|
|
||||||
pth = s.pathHistory[len(s.pathHistory)-2]
|
|
||||||
s.pathHistory = s.pathHistory[:len(s.pathHistory)-1]
|
|
||||||
}
|
|
||||||
// Getting the screen and changing to
|
|
||||||
// then executing its widget.
|
|
||||||
if !pth.IsAbs() {
|
|
||||||
pth = (s.Path() + "/" + pth).Clean()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.PathExist(pth) {
|
|
||||||
return ScreenNotExistErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if !back && s.Path() != pth {
|
|
||||||
s.pathHistory = append(s.pathHistory, pth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stopping the current widget.
|
|
||||||
screen := s.bot.behaviour.Screens[pth]
|
|
||||||
s.skippedUpdates.Close()
|
|
||||||
|
|
||||||
if screen.Widget != nil {
|
|
||||||
s.skippedUpdates, err = s.runWidget(screen.Widget, arg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return NoWidgetForScreenErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) runCompo(compo Component, arg any) (*UpdateChan, error) {
|
|
||||||
if compo == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
s, ok := compo.(Sendable)
|
|
||||||
if ok {
|
|
||||||
msg, err := c.Send(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.SetMessage(msg)
|
|
||||||
}
|
|
||||||
updates := NewUpdateChan()
|
|
||||||
go func() {
|
|
||||||
compo.Serve(
|
|
||||||
c.WithInput(updates).
|
|
||||||
WithArg(arg),
|
|
||||||
)
|
|
||||||
// To let widgets finish themselves before
|
|
||||||
// the channel is closed and close it by themselves.
|
|
||||||
updates.Close()
|
|
||||||
}()
|
|
||||||
return updates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run widget in background returning the new input channel for it.
|
|
||||||
func (c *Context) runWidget(widget Widget, arg any) (*UpdateChan, error) {
|
|
||||||
var err error
|
|
||||||
if widget == nil {
|
|
||||||
return nil, EmptyWidgetErr
|
|
||||||
}
|
|
||||||
|
|
||||||
pth := c.Path()
|
|
||||||
compos := widget.Render(c.WithArg(c.makeArg(args)))
|
|
||||||
// Leave if changed path or components are empty.
|
|
||||||
if compos == nil || pth != c.Path() {
|
|
||||||
return nil, EmptyCompoErr
|
|
||||||
}
|
|
||||||
chns := make([]*UpdateChan, len(compos))
|
|
||||||
for i, compo := range compos {
|
|
||||||
chns[i], err = c.runCompo(compo, arg)
|
|
||||||
if err != nil {
|
|
||||||
for _, chn := range chns {
|
|
||||||
chn.Close()
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := NewUpdateChan()
|
|
||||||
go func() {
|
|
||||||
ln := len(compos)
|
|
||||||
UPDATE:
|
|
||||||
for u := range ret.Chan() {
|
|
||||||
if u == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cnt := 0
|
|
||||||
for i, compo := range compos {
|
|
||||||
chn := chns[i]
|
|
||||||
if chn.Closed() {
|
|
||||||
cnt++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !compo.Filter(u) {
|
|
||||||
chn.Send(u)
|
|
||||||
continue UPDATE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cnt == ln {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret.Close()
|
|
||||||
for _, chn := range chns {
|
|
||||||
chn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
version: 3
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
cmds:
|
|
||||||
- go build -o testbot ./cmd/test/
|
|
6
ui.go
6
ui.go
|
@ -3,12 +3,12 @@ package tg
|
||||||
// The type describes dynamic screen widget
|
// The type describes dynamic screen widget
|
||||||
// That can have multiple UI components.
|
// That can have multiple UI components.
|
||||||
type Widget interface {
|
type Widget interface {
|
||||||
Render(*Context) UI
|
Render(Context) UI
|
||||||
}
|
}
|
||||||
|
|
||||||
// The way to describe custom function based Widgets.
|
// The way to describe custom function based Widgets.
|
||||||
type RenderFunc func(c *Context) UI
|
type RenderFunc func(c Context) UI
|
||||||
func (fn RenderFunc) Render(c *Context) UI {
|
func (fn RenderFunc) Render(c Context) UI {
|
||||||
return fn(c)
|
return fn(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
update.go
26
update.go
|
@ -2,7 +2,6 @@ package tg
|
||||||
|
|
||||||
import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
|
||||||
type FileId string
|
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
tgbotapi.Update
|
tgbotapi.Update
|
||||||
|
@ -16,18 +15,18 @@ type UpdateChan struct {
|
||||||
// Return new update channel.
|
// Return new update channel.
|
||||||
func NewUpdateChan() *UpdateChan {
|
func NewUpdateChan() *UpdateChan {
|
||||||
ret := &UpdateChan{}
|
ret := &UpdateChan{}
|
||||||
ret.chn = make(chan *Update)
|
ret.chn = make(chan Update)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (updates *UpdateChan) Chan() chan *Update {
|
func (updates *UpdateChan) Chan() chan Update {
|
||||||
return updates.chn
|
return updates.chn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an update to the channel.
|
// Send an update to the channel.
|
||||||
// Returns true if the update was sent.
|
// Returns true if the update was sent.
|
||||||
func (updates *UpdateChan) Send(u *Update) bool {
|
func (updates *UpdateChan) Send(u Update) bool {
|
||||||
defer recover()
|
defer recover()
|
||||||
if updates == nil || updates.chn == nil {
|
if updates == nil || updates.chn == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -37,11 +36,11 @@ func (updates *UpdateChan) Send(u *Update) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read an update from the channel.
|
// Read an update from the channel.
|
||||||
func (updates *UpdateChan) Read() *Update {
|
func (updates *UpdateChan) Read() (Update, bool) {
|
||||||
if updates == nil || updates.chn == nil {
|
if updates == nil || updates.chn == nil {
|
||||||
return nil
|
return Update{}, false
|
||||||
}
|
}
|
||||||
return <-updates.chn
|
return <-updates.chn, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the channel is closed.
|
// Returns true if the channel is closed.
|
||||||
|
@ -60,13 +59,12 @@ func (updates *UpdateChan) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Update) HasDocument() bool {
|
func (u Update) HasDocument() bool {
|
||||||
return u != nil &&
|
return u.Message != nil &&
|
||||||
u.Message != nil &&
|
|
||||||
u.Message.Document != nil
|
u.Message.Document != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Update) DocumentId() FileId {
|
func (u Update) DocumentID() FileID {
|
||||||
return FileId(u.Update.Message.Document.FileID)
|
return FileID(u.Update.Message.Document.FileID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Update) DocumentName() string {
|
func (u *Update) DocumentName() string {
|
||||||
|
@ -86,10 +84,10 @@ func (u Update) HasPhotos() bool {
|
||||||
len(u.Message.Photo) != 0
|
len(u.Message.Photo) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Update) PhotoIds() []FileId {
|
func (u Update) PhotoIDs() []FileID {
|
||||||
ret := make([]FileId, len(u.Message.Photo))
|
ret := make([]FileID, len(u.Message.Photo))
|
||||||
for i, photo := range u.Message.Photo {
|
for i, photo := range u.Message.Photo {
|
||||||
ret[i] = FileId(photo.FileID)
|
ret[i] = FileID(photo.FileID)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
9
updater.go
Normal file
9
updater.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package tg
|
||||||
|
|
||||||
|
// Implementing the type provides
|
||||||
|
// way to update stuff on the client side.
|
||||||
|
// Things like panels, messages etc.
|
||||||
|
type Updater interface {
|
||||||
|
Update(Context) error
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue