diff --git a/beh.go b/beh.go
index 121a034..c40c8f0 100644
--- a/beh.go
+++ b/beh.go
@@ -1,13 +1,10 @@
package tg
-// The package implements
-// behaviour for the Telegram bots.
-
// The type describes behaviour for the bot in personal chats.
type Behaviour struct {
Root Component
- Init Action
- Screens ScreenMap
+ Init Action
+ Screens ScreenMap
}
// Returns new empty behaviour.
@@ -19,43 +16,21 @@ func NewBehaviour() *Behaviour {
// The Action will be called on session creation,
// 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
return b
}
-// Alias to WithInit to simplify behaviour definitions.
-func (b *Behaviour) WithInitFunc(
- fn ActionFunc,
-) *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()
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
// and its commands..
-func (b *Behaviour) WithRoot(root Component) *Behaviour {
+func (b *Behaviour) SetRootWidget(root Component) *Behaviour {
b.Root = root
return b
}
diff --git a/bot.go b/bot.go
index fb6af85..d5cce6d 100644
--- a/bot.go
+++ b/bot.go
@@ -38,7 +38,7 @@ func NewBot(token string) (*Bot, error) {
}, nil
}
-func (bot *Bot) Debug(debug bool) *Bot {
+func (bot *Bot) SetDebug(debug bool) *Bot {
bot.api.Debug = debug
return bot
}
@@ -56,27 +56,26 @@ func (bot *Bot) Me() User {
// SessionId represents both for chat IDs.
func (bot *Bot) Send(
sid SessionId, v Sendable,
-) (Message, error) {
+) (*Message, error) {
config := v.SendConfig(sid, bot)
if config.Error != nil {
- return Message{}, config.Error
+ return nil, config.Error
}
msg, err := bot.api.Send(config.ToApi())
if err != nil {
- return Message{}, err
+ return nil, err
}
- v.SetMessage(msg)
- return msg, nil
+ v.SetMessage(&msg)
+ return &msg, nil
}
func (bot *Bot) Sendf(
sid SessionId, format string, v ...any,
-) (Message, error){
- msg := Messagef(format, v...)
+) (*Message, error){
return bot.Send(
sid,
- &msg,
+ Messagef(format, v...),
)
}
@@ -93,20 +92,29 @@ func (bot *Bot) SendRaw(
// Get session by its ID. Can be used for any scope
// including private, group and channel.
-func (bot *Bot) GetSession(
+func (bot *Bot) GotSession(
sid SessionId,
) (*Session, bool) {
session, ok := bot.sessions[sid]
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.sessions = make(SessionMap)
return b
}
-func (b *Bot) WithSessions(sessions SessionMap) *Bot {
+func (b *Bot) SetSessions(sessions SessionMap) *Bot {
b.sessions = sessions
return b
}
@@ -140,7 +148,7 @@ func (bot *Bot) SetCommands(
}
sort.Strings(names)
- cmds := []*Command{}
+ cmds := []Command{}
for _, name := range names {
cmds = append(
cmds,
@@ -202,7 +210,7 @@ func (bot *Bot) Run() error {
go bot.handleGroup(chn)
}*/
- me, _ := bot.Api.GetMe()
+ me, _ := bot.Api().GetMe()
bot.me = me
for up := range updates {
u := Update{
@@ -216,6 +224,7 @@ func (bot *Bot) Run() error {
}
chn, ok := handles[fromChat.Type]
+ // Skipping non existent scope.
if !ok {
continue
}
@@ -232,64 +241,30 @@ func (bot *Bot) handlePrivate(updates chan Update) {
var sid SessionId
for u := range updates {
sid = SessionId(u.FromChat().ID)
- ctx, ctxOk := bot.contexts[sid]
- if u.Message != nil && !ctxOk {
+ session, sessionOk := bot.sessions[sid]
- session, sessionOk := bot.sessions[sid]
- if !sessionOk {
- // Creating session if we have none.
- session = bot.sessions.Add(sid, PrivateSessionScope)
- }
- session = bot.sessions[sid]
+ if u.Message != nil && !sessionOk {
+ // Creating session if we have none
+ // but only on text messages.
+ session = bot.sessions.Add(bot, sid, PrivateSessionScope)
- // 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{
+ // Creating the root context
+ // that takes updates directly from
+ // the session.
+ rootContext := Context{
session: session,
- bot: bot,
- Update: u,
- input: ctx.updates,
- }.serve()
- ctx.session.updates.Send(u)
+ update: u,
+ input: session.updates,
+ }
+ go rootContext.serve()
+ rootContext.input.Send(u)
+
continue
}
- if ctxOk {
- ctx.updates.Send(u)
+ if sessionOk {
+ 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
- }
-}
-*/
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..ad0cf18
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+#
+go build -o ./exe/test ./cmd/test
diff --git a/button.go b/button.go
index 4573bb8..d921ccd 100644
--- a/button.go
+++ b/button.go
@@ -14,31 +14,37 @@ type Button struct {
Url string
SendLocation bool
Action Action
+ // Used to skip buttons in generating by functions.
+ Valid bool
}
type ButtonMap map[string]Button
-// Returns the only location button in the map.
-func (btnMap ButtonMap) LocationButton() *Button {
+// Returns the only location button in the map and if it is there at all.
+// The location map must be the ONLY one.
+
+func (btnMap ButtonMap) LocationButton() (Button, bool) {
for _, btn := range btnMap {
if btn.SendLocation {
- return btn
+ return btn, true
}
}
- return nil
+ return Button{}, false
}
// Represents the reply button row.
-type ButtonRow []*Button
+type ButtonRow []Button
// Returns new button with the specified text and no action.
func Buttonf(format string, v ...any) Button {
- return &Button{
+ return Button{
Text: fmt.Sprintf(format, v...),
+ Valid: true,
}
}
// Randomize buttons data to make the key unique.
+// No guaranties about collisions though.
func (btn Button) Rand() Button {
rData := make([]byte, 8)
rand.Read(rData)
@@ -72,10 +78,16 @@ func (btn Button) WithSendLocation(ok bool) Button {
return btn
}
-func (btn Button) Go(pth Path, args ...any) Button {
+func (btn Button) Go(pth Path) Button {
return btn.WithAction(ScreenGo{
Path: pth,
- Args: args,
+ })
+}
+
+func (btn Button) GoWithArg(pth Path, arg any) Button {
+ return btn.WithAction(ScreenGo{
+ Path: pth,
+ Arg: arg,
})
}
@@ -89,11 +101,17 @@ func (btn Button) ToTelegram() apix.KeyboardButton {
func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
if btn.Data != "" {
- return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data)
+ return apix.NewInlineKeyboardButtonData(
+ btn.Text,
+ btn.Data,
+ )
}
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.
@@ -102,9 +120,6 @@ func (btn Button) ToTelegramInline() apix.InlineKeyboardButton {
// Return the key of the button to identify it by messages and callbacks.
func (btn Button) Key() string {
- if btn == nil {
- return ""
- }
if btn.Data != "" {
return btn.Data
}
@@ -113,6 +128,7 @@ func (btn Button) Key() string {
return btn.Text
}
-func NewButtonRow(btns ...*Button) ButtonRow {
+func NewButtonRow(btns ...Button) ButtonRow {
return btns
}
+
diff --git a/cmd/test/cmd.go b/cmd/test/cmd.go
new file mode 100644
index 0000000..4f78c11
--- /dev/null
+++ b/cmd/test/cmd.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "vultras.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(StartAbsPath),
+ tg.NewCommand(
+ "info",
+ "info desc",
+ ).WithAction(tg.Func(func(c tg.Context) {
+ c.SendfHTML(`cockcock 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,
+ ),
+ )
+ })),
+}
+
diff --git a/cmd/test/incdec.go b/cmd/test/incdec.go
new file mode 100644
index 0000000..29a5b66
--- /dev/null
+++ b/cmd/test/incdec.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "vultras.su/core/tg"
+ "fmt"
+)
+
+const (
+ IncDecPath tg.Path = "inc-dec"
+)
+
+var IncDecWidget = 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.Buttonf("+").WithAction(tg.Func(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.Buttonf("-").WithAction(tg.Func(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.Messagef(format, d.Counter).Inline(inline)
+
+ return tg.UI{
+ kbd,
+ tg.Messagef("Use the reply keyboard to get back").Reply(
+ BackKeyboard.Reply(),
+ ),
+ }
+})
+
diff --git a/cmd/test/keyboard.go b/cmd/test/keyboard.go
new file mode 100644
index 0000000..56dbff8
--- /dev/null
+++ b/cmd/test/keyboard.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "vultras.su/core/tg"
+)
+
+var HomeButton = tg.Buttonf("Home").Go("/")
+var BackButton = tg.Buttonf("Back").Go("-")
+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()
diff --git a/cmd/test/location.go b/cmd/test/location.go
new file mode 100644
index 0000000..b2dd43c
--- /dev/null
+++ b/cmd/test/location.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "vultras.su/core/tg"
+)
+
+const (
+ LocationPath tg.Path = "location"
+)
+
+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,
+ ),
+ }
+})
diff --git a/cmd/test/main.go b/cmd/test/main.go
index dc32121..047615f 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -3,8 +3,6 @@ package main
import (
"log"
"os"
- "strings"
- "fmt"
"vultras.su/core/tg"
)
@@ -17,391 +15,72 @@ type SessionData struct {
Counter int
}
-type MutateMessageWidget struct {
- Mutate func(string) string
+func ExtractSessionData(c tg.Context) *SessionData {
+ return c.SessionData().(*SessionData)
}
-func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
- ret := &MutateMessageWidget{}
- ret.Mutate = fn
- return ret
-}
-
-func (w *MutateMessageWidget) 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)
- }
- }
-}
-
-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(
+var beh = tg.NewBehaviour().SetInit(tg.Func(func(c tg.Context) {
+ // The session initialization.
+ c.SetSessionData(&SessionData{})
+})).SetRootNode(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)
- }
- }
- }),
- }
- }),
+ StartWidget,
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(),
- ),
- }
- }),
+ PanelPath,
+ PanelWidget,
),
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(),
- ),
- }
- }),
+ MutateMessagesPath,
+ MutateMessagesWidget,
+
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),
- }
- }),
+ UpperCasePath,
+ MutateMessagesToUpperCaseWidget,
),
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),
- }
- }),
+ LowerCasePath,
+ MutateMessagesToLowerCaseWidget,
),
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),
- }
- }),
+ EscapePath,
+ MutateMessagesEscapeWidget,
),
),
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(),
- ),
- }
- }),
+ IncDecPath,
+ IncDecWidget,
),
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,
- ),
- }
- }),
+ LocationPath,
+ LocationWidget,
),
-)).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(`cockcock 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) {
- })),
+)).SetRootWidget(tg.NewCommandCompo().SetUsage(
+ UsageAction,
+).SetPreStart(
+ PreStartAction,
+).SetCommands(
+ BotCommands...,
))
func main() {
- log.Println(beh.Screens)
token := os.Getenv("BOT_TOKEN")
bot, err := tg.NewBot(token)
if err != nil {
log.Panic(err)
}
- bot = bot.
- WithBehaviour(beh).
- Debug(true)
+ bot = bot.SetBehaviour(beh)
+ //SetDebug(true)
- bot.Data = &BotData{
+ bot.SetData(&BotData{
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()
if err != nil {
panic(err)
diff --git a/cmd/test/mutate.go b/cmd/test/mutate.go
new file mode 100644
index 0000000..50afce3
--- /dev/null
+++ b/cmd/test/mutate.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+ "vultras.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
+}
+
+const (
+ UpperCasePath tg.Path = "upper-case"
+ LowerCasePath = "lower-case"
+ EscapePath = "escape"
+ MutateMessagesPath = "mutate-messages"
+)
+
+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(UpperCasePath),
+ tg.Buttonf("Lower case").Go(LowerCasePath),
+ tg.Buttonf("Escape chars").Go(EscapePath),
+ ).Row(
+ BackButton,
+ ).Reply(),
+ ),
+ }
+})
+
+var MutateMessagesToLowerCaseWidget = 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 MutateMessagesToUpperCaseWidget = 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 MutateMessagesEscapeWidget = 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),
+ }
+})
+
diff --git a/cmd/test/panel.go b/cmd/test/panel.go
new file mode 100644
index 0000000..164ba02
--- /dev/null
+++ b/cmd/test/panel.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "vultras.su/core/tg"
+)
+
+const (
+ PanelPath tg.Path = "panel"
+)
+
+var PanelWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
+ var (
+ n = 0
+ ln = 4
+ panel *tg.PanelCompo
+ )
+
+ panel = tg.Messagef(
+ "Some panel",
+ ).Panel(c, tg.RowserFunc(func(c tg.Context) []tg.ButtonRow {
+ btns := []tg.ButtonRow{
+ tg.ButtonRow{tg.Buttonf("Page %d", n)},
+ }
+ for i := 0; i < ln; i++ {
+ num := 1 + n*ln + i
+ btns = append(btns, 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),
+ })
+ }
+ btns = append(btns, tg.ButtonRow{
+ tg.Buttonf("Prev").WithAction(tg.Func(func(c tg.Context) {
+ n--
+ panel.Update(c)
+ })),
+ tg.Buttonf("Next").WithAction(tg.Func(func(c tg.Context) {
+ n++
+ panel.Update(c)
+ })),
+ })
+
+ return btns
+ }))
+
+ return tg.UI{
+ panel,
+ tg.Messagef("").Reply(
+ BackKeyboard.Reply(),
+ ),
+ }
+})
diff --git a/cmd/test/start.go b/cmd/test/start.go
new file mode 100644
index 0000000..9174914
--- /dev/null
+++ b/cmd/test/start.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "vultras.su/core/tg"
+ "fmt"
+)
+
+const (
+ StartAbsPath tg.Path = "/"
+)
+var StartWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
+ return tg.UI{
+ tg.Messagef(
+ 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.CallbackUpdate().SentFrom().UserName,
+ ).Inline(
+ tg.NewKeyboard().Row(
+ tg.Buttonf("TeleGopher Vultras page").
+ WithUrl("https://vultras.su/core/tg"),
+ ).Inline(),
+ ),
+
+ tg.Messagef("Choose your interest").Reply(
+ tg.NewKeyboard().Row(
+ tg.Buttonf("Inc/Dec").Go(IncDecPath),
+ ).Row(
+ tg.Buttonf("Mutate messages").Go(MutateMessagesPath),
+ ).Row(
+ tg.Buttonf("Send location").Go(LocationPath),
+ ).Row(
+ tg.Buttonf("Dynamic panel").Go(PanelPath),
+ ).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)
+ }
+ }
+ }),
+ }
+})
diff --git a/command.go b/command.go
index b1ea1c1..e1adbe4 100644
--- a/command.go
+++ b/command.go
@@ -18,21 +18,22 @@ type Command struct {
Description string
Action Action
Widget Widget
+ WidgetArg any
}
-type CommandMap map[CommandName]*Command
+type CommandMap map[CommandName]Command
func NewCommand(name CommandName, desc string) Command {
if name == "" || desc == "" {
panic("name and description cannot be an empty string")
}
- return &Command{
+ return Command{
Name: name,
Description: desc,
}
}
-func (c *Command) WithAction(a Action) *Command {
+func (c Command) WithAction(a Action) Command {
c.Action = a
return c
}
@@ -42,10 +43,7 @@ func (c Command) WithWidget(w Widget) Command {
return c
}
-func (c Command) WidgetFunc(fn Func) Command {
- return c.WithWidget(fn)
-}
-
+// Convert command into the tgbotapi.BotCommand
func (c Command) ToApi() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name)
@@ -53,10 +51,17 @@ func (c Command) ToApi() tgbotapi.BotCommand {
return ret
}
-func (c Command) Go(pth Path, args ...any) Command {
+// Simple command to go to another screen.
+func (c Command) Go(pth Path) Command {
return c.WithAction(ScreenGo{
Path: pth,
- Args: args,
+ })
+}
+
+func (c Command) GoWithArg(pth Path, arg any) Command {
+ return c.WithAction(ScreenGo{
+ Path: pth,
+ Arg: arg,
})
}
@@ -69,13 +74,13 @@ type CommandCompo struct {
}
// Returns new empty CommandCompo.
-func NewCommandCompo(cmds ...*Command) *CommandCompo {
- ret := CommandCompo{}.WithCommands(cmds...)
+func NewCommandCompo(cmds ...Command) *CommandCompo {
+ ret := (&CommandCompo{}).SetCommands(cmds...)
return ret
}
// Set the commands to handle.
-func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo {
+func (w *CommandCompo) SetCommands(cmds ...Command) *CommandCompo {
if w.Commands == nil {
w.Commands = make(CommandMap)
}
@@ -93,24 +98,20 @@ func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo {
}
// Set the prestart action.
-func (w *CommandCompo) WithPreStart(a Action) *CommandCompo {
+func (w *CommandCompo) SetPreStart(a Action) *CommandCompo {
w.PreStart = a
return w
}
// Set the usage action.
-func (w CommandCompo) WithUsage(a Action) *CommandCompo {
+func (w *CommandCompo) SetUsage(a Action) *CommandCompo {
w.Usage = a
return w
}
-// Set the usage action with function.
-func (w CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo {
- return w.WithUsage(fn)
-}
-
-func (widget CommandCompo) Filter(
- u *Update,
+// Filtering all the non commands.
+func (widget *CommandCompo) Filter(
+ u Update,
) bool {
if u.Message == nil || !u.Message.IsCommand() {
return false
@@ -120,16 +121,14 @@ func (widget CommandCompo) Filter(
}
// Implementing server.
-func (compo CommandCompo) Serve(c Context) {
- /*commanders := make(map[CommandName] BotCommander)
- for k, v := range compo.Commands {
- commanders[k] = v
- }*/
- c.bot.DeleteCommands()
- err := c.bot.SetCommands(
- tgbotapi.NewBotCommandScopeChat(c.Session.Id.ToApi()),
+func (compo *CommandCompo) Serve(c Context) {
+ // First should bring the new command into the action.
+ c.Bot().DeleteCommands()
+ err := c.Bot().SetCommands(
+ tgbotapi.NewBotCommandScopeChat(c.SessionId().ToApi()),
compo.Commands,
)
+
if err != nil {
c.Sendf("error: %q", err)
}
@@ -157,8 +156,10 @@ func (compo CommandCompo) Serve(c Context) {
c.WithUpdate(u).Run(cmd.Action)
if cmd.Widget != nil {
+ // Closing current widget
cmdUpdates.Close()
- cmdUpdates, _ = c.WithUpdate(u).RunWidget(cmd.Widget)
+ // And running the other one.
+ cmdUpdates, _ = c.runWidget(cmd.Widget, cmd.WidgetArg)
}
continue
}
@@ -168,7 +169,7 @@ func (compo CommandCompo) Serve(c Context) {
// executing one.
cmdUpdates.Send(u)
} else {
- c.Skip(u)
+ c.SkipUpdate(u)
}
}
}
diff --git a/context.go b/context.go
index 62d7ebf..0e35b4c 100644
--- a/context.go
+++ b/context.go
@@ -11,6 +11,8 @@ import (
// Interface to interact with the user.
type Context struct {
+ // The session contains all
+ // the information between the contexts.
session *Session
// The update that called the Context usage.
update Update
@@ -27,15 +29,15 @@ type Context struct {
// make other user to leave the bot at first but
// maybe you will find another usage for this.
// Returns users context by specified session ID
-// or nil if the user is not logged in.
-func (c Context) As(sid SessionId) Context {
- n, ok := c.Bot.contexts[sid]
+// or false if the user is not logged in.
+func (c Context) As(sid SessionId) (Context, bool) {
+ s, ok := c.Bot().GotSession(sid)
if !ok {
- return nil
- }
- return &Context{
- context: n,
+ return Context{}, false
}
+
+ c.session = s
+ return c, true
}
// General type function to define actions, single component widgets
@@ -47,10 +49,10 @@ func (f Func) Act(c Context) {
func (f Func) Serve(c Context) {
f(c)
}
-func(f Func) Filter(_ *Update) bool {
+func(f Func) Filter(_ Update) bool {
return false
}
-func (f Func) Render(_ *Context) UI {
+func (f Func) Render(_ Context) UI {
return UI{
f,
}
@@ -65,19 +67,11 @@ const (
// Goroutie function to handle each user.
func (c Context) serve() {
- beh := c.Bot.behaviour
+ beh := c.Bot().behaviour
c.Run(beh.Init)
beh.Root.Serve(c)
}
-func (c Context) Path() Path {
- ln := len(c.pathHistory)
- if ln == 0 {
- return ""
- }
- return c.pathHistory[ln-1]
-}
-
func (c Context) Arg() any {
return c.arg
}
@@ -88,21 +82,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.
-func (c Context) Send(v Sendable) (Message, error) {
- config := v.SendConfig(c.Session.Id, c.Bot)
+// Sends to the Sendable object to the session user.
+func (c Context) Send(v Sendable) (*Message, error) {
+ config := v.SendConfig(c.SessionId(), c.Bot())
if config.Error != nil {
return nil, config.Error
}
- msg, err := c.Bot.Api.Send(config.ToApi())
+ msg, err := c.Bot().Api().Send(config.ToApi())
if err != nil {
return nil, err
}
@@ -111,23 +99,23 @@ func (c Context) Send(v Sendable) (Message, error) {
// Sends the formatted with fmt.Sprintf message to the user
// using default Markdown parsing format.
-func (c Context) Sendf(format string, v ...any) (Message, error) {
- return c.Send(NewMessage(format, v...))
+func (c Context) Sendf(format string, v ...any) (*Message, error) {
+ return c.Send(Messagef(format, v...))
}
// Same as Sendf but uses Markdown 2 format for parsing.
-func (c Context) Sendf2(format string, v ...any) (Message, error) {
- return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2())
+func (c Context) Sendf2(format string, v ...any) (*Message, error) {
+ return c.Send(Messagef(format, v...).MD2())
}
// Same as Sendf but uses HTML format for parsing.
-func (c Context) SendfHTML(format string, v ...any) (Message, error) {
- return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML())
+func (c Context) SendfHTML(format string, v ...any) (*Message, error) {
+ return c.Send(Messagef(format, v...).HTML())
}
// Send the message in raw format escaping all the special characters.
-func (c Context) SendfR(format string, v ...any) (Message, error) {
- return c.Send(NewMessage(Escape2(fmt.Sprintf(format, v...))).MD2())
+func (c Context) SendfR(format string, v ...any) (*Message, error) {
+ return c.Send(Messagef("%s", Escape2(fmt.Sprintf(format, v...))).MD2())
}
// Get the input for current widget.
@@ -141,8 +129,8 @@ func (c Context) WithArg(v any) Context {
return c
}
-func (c Context) WithUpdate(u *Update) Context {
- c.Update = u
+func (c Context) WithUpdate(u Update) Context {
+ c.update = u
return c
}
@@ -151,14 +139,6 @@ func (c Context) WithInput(input *UpdateChan) Context {
return c
}
-func (c Context) Go(pth Path) error {
- return c.session.go_(pth, nil)
-}
-
-func (c Context) GoWithArg(pth Path, arg any) error {
- return c.session.go_(pth, arg)
-}
-
// Customized actions for the bot.
type Action interface {
Act(Context)
@@ -166,29 +146,18 @@ type Action interface {
type ActionFunc func(Context)
-func (af ActionFunc) Act(c *Context) {
+func (af ActionFunc) Act(c Context) {
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
// the specified prompt.
func (c Context) ReadString(promptf string, args ...any) string {
var text string
- if pref != "" {
+ if promptf != "" {
c.Sendf(promptf, args...)
}
for u := range c.Input() {
- if u == nil {
- break
- }
if u.Message == nil {
continue
}
@@ -198,19 +167,19 @@ func (c Context) ReadString(promptf string, args ...any) string {
return text
}
-func (c Context) Update() Update {
- return c.update
+func (c Context) CallbackUpdate() *Update {
+ return &c.update
}
// Returns the reader for specified file ID and path.
-func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
- file, err := c.Bot.Api.GetFile(tgbotapi.FileConfig{FileID:string(fileId)})
+func (c Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
+ file, err := c.Bot().Api().GetFile(tgbotapi.FileConfig{FileID:string(fileId)})
if err != nil {
return nil, "", err
}
r, err := http.Get(fmt.Sprintf(
"https://api.telegram.org/file/bot%s/%s",
- c.Bot.Api.Token,
+ c.Bot().Api().Token,
file.FilePath,
))
if err != nil {
@@ -223,7 +192,8 @@ func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
return r.Body, file.FilePath, nil
}
-func (c *Context) ReadFile(fileId FileId) ([]byte, string, error) {
+// Reads all the content from the specified file.
+func (c Context) ReadFile(fileId FileId) ([]byte, string, error) {
file, pth, err := c.GetFile(fileId)
if err != nil {
return nil, "", err
@@ -238,3 +208,184 @@ func (c *Context) ReadFile(fileId FileId) ([]byte, string, error) {
return bts, pth, nil
}
+func (c Context) runCompo(compo Component, arg any) (*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).
+ 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(arg))
+ // 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)
+ //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
+}
+
+// Simple go without an argument for context.
+func (c Context) Go(pth Path) error {
+ return c.GoWithArg(pth, nil)
+}
+
+// Changes screen of user to the Id one.
+// Also gives the arg to the widget calling
+// contexts.
+func (c Context) GoWithArg(pth Path, arg any) error {
+ var err error
+ if pth == "" {
+ c.session.pathHistory = []Path{}
+ return nil
+ }
+ var back bool
+ if pth == "-" {
+ if len(c.session.pathHistory) < 2 {
+ return c.GoWithArg("", arg)
+ }
+ pth = c.session.pathHistory[len(c.session.pathHistory)-2]
+ c.session.pathHistory = c.session.pathHistory[:len(c.session.pathHistory)-1]
+ }
+ // Getting the screen and changing to
+ // then executing its widget.
+ if !pth.IsAbs() {
+ pth = (c.Path() + "/" + pth).Clean()
+ }
+
+ if !c.PathExist(pth) {
+ return ScreenNotExistErr
+ }
+
+ if !back && c.Path() != pth {
+ c.session.pathHistory = append(c.session.pathHistory, pth)
+ }
+
+ // Stopping the current widget.
+ screen := c.Bot().behaviour.Screens[pth]
+ c.session.skippedUpdates.Close()
+
+ if screen.Widget != nil {
+ c.session.skippedUpdates, err = c.runWidget(screen.Widget, arg)
+ if err != nil {
+ return err
+ }
+ } else {
+ return NoWidgetForScreenErr
+ }
+
+ 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
+}
+
+// Returns true if the path exists and false otherwise.
+func (c Context) PathExist(pth Path) bool {
+ return c.session.bot.behaviour.PathExist(pth)
+}
+
+// Return context's session's path history.
+func (c Context) PathHistory() []Path {
+ return c.session.pathHistory
+}
+
+func (c Context) Path() Path {
+ ln := len(c.session.pathHistory)
+ if ln == 0 {
+ return ""
+ }
+ return c.session.pathHistory[ln-1]
+}
diff --git a/file.go b/file.go
index 86ff522..c198398 100644
--- a/file.go
+++ b/file.go
@@ -40,7 +40,7 @@ type File struct {
func NewFile(reader io.Reader) *File {
ret := &File{}
- ret.MessageCompo = NewMessage("")
+ ret.MessageCompo = *Messagef("")
ret.reader = reader
ret.upload = true
@@ -117,11 +117,11 @@ func (f *File) SendConfig(
photo := tgbotapi.NewPhoto(cid, f)
photo.Caption = f.caption
- config.Photo = &photo
+ config.Chattable = photo
case DocumentFileType:
doc := tgbotapi.NewDocument(sid.ToApi(), f)
doc.Caption = f.caption
- config.Document = &doc
+ config.Chattable = doc
default:
panic(UnknownFileTypeErr)
}
diff --git a/filter.go b/filter.go
index cafe364..d380577 100644
--- a/filter.go
+++ b/filter.go
@@ -7,12 +7,12 @@ package tg
type Filterer interface {
// Return true if should filter the update
// 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(
- u *Update,
+ u Update,
) bool {
return f(u)
}
diff --git a/inline.go b/inline.go
index dc1a700..5c11352 100644
--- a/inline.go
+++ b/inline.go
@@ -19,7 +19,7 @@ func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
}
buttons := []tgbotapi.InlineKeyboardButton{}
for _, button := range row {
- if button == nil {
+ if !button.Valid {
continue
}
buttons = append(buttons, button.ToTelegramInline())
@@ -41,9 +41,11 @@ func (compo *InlineCompo) SendConfig(
sid SessionId, bot *Bot,
) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot)
+ msg := sendConfig.Chattable.(tgbotapi.MessageConfig)
if len(compo.Inline.Rows) > 0 {
- sendConfig.Message.ReplyMarkup = compo.Inline.ToApi()
+ msg.ReplyMarkup = compo.Inline.ToApi()
}
+ sendConfig.Chattable = msg
return sendConfig
}
@@ -51,34 +53,39 @@ func (compo *InlineCompo) SendConfig(
// Update the component on the client side.
// Requires exactly the pointer but not the value
// cause it changes insides of the structure.
-func (compo *InlineCompo) Update(c Context) {
+func (compo *InlineCompo) Update(c Context) error {
if compo.Message != nil {
var edit tgbotapi.Chattable
markup := compo.Inline.ToApi()
ln := len(markup.InlineKeyboard)
if ln == 0 || compo.Inline.Rows == nil {
edit = tgbotapi.NewEditMessageText(
- c.Session.Id.ToApi(),
+ c.SessionId().ToApi(),
compo.Message.MessageID,
compo.Text,
)
} else {
edit = tgbotapi.NewEditMessageTextAndMarkup(
- c.Session.Id.ToApi(),
+ c.SessionId().ToApi(),
compo.Message.MessageID,
compo.Text,
markup,
)
}
- msg, _ := c.Bot.Api.Send(edit)
+ msg, err := c.Bot().Api().Send(edit)
+ if err != nil {
+ return err
+ }
compo.Message = &msg
}
- compo.buttonMap = compo.MakeButtonMap()
+
+
+ return nil
}
// Implementing the Filterer interface.
-func (compo InlineCompo) Filter(u Update) bool {
- if compo == nil || u.CallbackQuery == nil {
+func (compo *InlineCompo) Filter(u Update) bool {
+ if u.CallbackQuery == nil {
return true
}
@@ -91,13 +98,13 @@ func (compo InlineCompo) Filter(u Update) bool {
}
// Implementing the Server interface.
-func (compo InlineCompo) Serve(c Context) {
+func (compo *InlineCompo) Serve(c Context) {
for u := range c.Input() {
compo.OnOneUpdate(c, u)
}
}
-func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
+func (compo *InlineCompo) OnOneUpdate(c Context, u Update) error {
var act Action
btns := compo.ButtonMap()
cb := tgbotapi.NewCallback(
@@ -106,20 +113,23 @@ func (compo *InlineCompo) OnOneUpdate(c Context, u Update) {
)
data := u.CallbackQuery.Data
- _, err := c.Bot.Api.Request(cb)
+ _, err := c.Bot().Api().Request(cb)
if err != nil {
- return
+ return err
}
btn, ok := btns[data]
if !ok {
- return
+ return nil
}
- if btn != nil {
+
+ if btn.Action != nil {
act = btn.Action
} else if compo.Action != nil {
act = compo.Action
}
c.WithUpdate(u).Run(act)
+
+ return nil
}
diff --git a/keyboard.go b/keyboard.go
index 440cdc2..24a35c6 100644
--- a/keyboard.go
+++ b/keyboard.go
@@ -10,12 +10,11 @@ type Keyboard struct {
// defined action for the button.
Action Action
Rows []ButtonRow
- buttonMap ButtonMap
}
// Returns the new keyboard with specified rows.
func NewKeyboard(rows ...ButtonRow) Keyboard {
- ret := &Keyboard{}
+ ret := Keyboard{}
for _, row := range rows {
if row != nil && len(row) > 0 {
ret.Rows = append(ret.Rows, row)
@@ -41,9 +40,10 @@ func (kbd Keyboard) Row(btns ...Button) Keyboard {
if len(btns) < 1 {
return kbd
}
- retBtns := []*Button{}
+
+ retBtns := make([]Button, 0, len(btns))
for _, btn := range btns {
- if btn == nil {
+ if !btn.Valid {
continue
}
retBtns = append(retBtns, btn)
@@ -58,7 +58,7 @@ func (kbd Keyboard) Row(btns ...Button) Keyboard {
// Adds buttons as one column list.
func (kbd Keyboard) List(btns ...Button) Keyboard {
for _, btn := range btns {
- if btn == nil {
+ if !btn.Valid {
continue
}
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
// is button data and the value is Action.
func (kbd Keyboard) ButtonMap() ButtonMap {
- if kbd.buttonMap == nil {
- kbd.buttonMap = kbd.MakeButtonMap()
- }
- return kbd.buttonMap
+ return kbd.MakeButtonMap()
}
// 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
}
}
- kbd.buttonMap = ret
return ret
}
@@ -103,8 +99,9 @@ func (kbd Keyboard) Inline() Inline {
}
// Convert the keyboard to the more specific reply one.
+// By default OneTime = true.
func (kbd Keyboard) Reply() Reply {
- ret := &Reply{}
+ ret := Reply{}
ret.Keyboard = kbd
// it is used more often than not once.
ret.OneTime = true
diff --git a/location.go b/location.go
index c01b5ea..9bca927 100644
--- a/location.go
+++ b/location.go
@@ -7,22 +7,22 @@ import (
type Location = tgbotapi.Location
type LocationCompo struct {
- *MessageCompo
+ MessageCompo
Location
}
func (compo *LocationCompo) SendConfig(
sid SessionId, bot *Bot,
-) (*SendConfig) {
+) (SendConfig) {
cid := sid.ToApi()
- loc := tgbotapi.NewLocation(
+ location := tgbotapi.NewLocation(
cid,
compo.Latitude,
compo.Longitude,
)
- ret := &SendConfig{
- Location: &loc,
- }
+
+ ret := SendConfig{}
+ ret.Chattable = location
return ret
}
diff --git a/message.go b/message.go
index a6fe070..d98ccc6 100644
--- a/message.go
+++ b/message.go
@@ -10,8 +10,13 @@ type Message = tgbotapi.Message
// Simple text message component type.
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
+ // The text to display.
Text string
}
@@ -29,91 +34,93 @@ func Escape2(str string) string {
// Call the function after the message was sent.
func (compo *MessageCompo) Update(c Context) error {
edit := tgbotapi.NewEditMessageText(
- c.Session.Id.ToApi(),
+ c.Session().Id.ToApi(),
compo.Message.MessageID,
compo.Text,
)
- msg, err := c.bot.api.Send(edit)
+ msg, err := c.Bot().Api().Send(edit)
if err != nil {
return err
}
- compo.Message = msg
+ compo.Message = &msg
return nil
}
-func (compo *MessageCompo) Delete(c *Context) {
- cfg := tgbotapi.NewDeleteMessage(c.Session.Id.ToApi(), compo.Message.MessageID)
- c.Bot.Api.Send(cfg)
- //c.Sendf("%q", err)
+// Calling the method removes the message on the client side
+// and sets the Message in the component to nil.
+func (compo *MessageCompo) Delete(c Context) error {
+ 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 of rendering functions.
-func (compo *MessageCompo) SetMessage(msg Message) {
- compo.Message = msg
-}
-
-// Return new message with the specified text.
-func Messagef(format string, v ...any) MessageCompo {
- ret := MessageCompo{}
+// Return new message with the specified text
+// formatted with the fmt.Sprintf function.
+func Messagef(format string, v ...any) *MessageCompo {
+ ret := &MessageCompo{}
ret.Text = fmt.Sprintf(format, v...)
ret.ParseMode = tgbotapi.ModeMarkdown
return ret
}
// Return message with the specified parse mode.
-func (msg MessageCompo) withParseMode(mode string) MessageCompo {
- msg.ParseMode = mode
- return msg
+func (compo *MessageCompo) setParseMode(mode string) *MessageCompo {
+ compo.ParseMode = mode
+ return compo
}
// Set the default Markdown parsing mode.
-func (msg MessageCompo) MD() MessageCompo {
- return msg.withParseMode(tgbotapi.ModeMarkdown)
+func (compo *MessageCompo) MD() *MessageCompo {
+ return compo.setParseMode(tgbotapi.ModeMarkdown)
}
// Set the Markdown 2 parsing mode.
-func (msg MessageCompo) MD2() MessageCompo {
- return msg.withParseMode(tgbotapi.ModeMarkdownV2)
+func (compo *MessageCompo) MD2() *MessageCompo {
+ return compo.setParseMode(tgbotapi.ModeMarkdownV2)
}
// Set the HTML parsing mode.
-func (msg MessageCompo) HTML() MessageCompo {
- return msg.withParseMode(tgbotapi.ModeHTML)
+func (compo *MessageCompo) HTML() *MessageCompo {
+ return compo.setParseMode(tgbotapi.ModeHTML)
}
// Transform the message component into one with reply keyboard.
-func (msg MessageCompo) Inline(inline Inline) InlineCompo {
- return InlineCompo{
+func (compo *MessageCompo) Inline(inline Inline) *InlineCompo {
+ return &InlineCompo{
Inline: inline,
- MessageCompo: msg,
+ MessageCompo: *compo,
}
}
// Transform the message component into one with reply keyboard.
-func (msg MessageCompo) Reply(reply Reply) ReplyCompo {
- return ReplyCompo{
+func (msg *MessageCompo) Reply(reply Reply) *ReplyCompo {
+ return &ReplyCompo{
Reply: reply,
- MessageCompo: msg,
+ MessageCompo: *msg,
}
}
// Transform the message component into the location one.
-func (msg MessageCompo) Location(
+func (msg *MessageCompo) Location(
lat, long float64,
-) LocationCompo {
- ret := &LocationCompo{
- MessageCompo: msg,
- Location: Location{
- Latitude: lat,
- Longitude: long,
- },
- }
+) *LocationCompo {
+ ret := &LocationCompo{}
+ ret.MessageCompo = *msg
+ ret.Latitude = lat
+ ret.Longitude = long
+
return ret
}
// Implementing the Sendable interface.
-func (config MessageCompo) SendConfig(
+func (compo *MessageCompo) SendConfig(
sid SessionId, bot *Bot,
) (SendConfig) {
var (
@@ -121,19 +128,27 @@ func (config MessageCompo) SendConfig(
text string
)
- if config.Text == "" {
+ // Protection against empty text,
+ // since it breaks the Telegram bot API.
+ if compo.Text == "" {
text = ">"
} else {
- text = config.Text
+ text = compo.Text
}
msg := tgbotapi.NewMessage(sid.ToApi(), text)
- msg.ParseMode = config.ParseMode
+ msg.ParseMode = compo.ParseMode
ret.Chattable = msg
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.
func (compo *MessageCompo) Serve(c Context) {}
diff --git a/panel.go b/panel.go
index e2e4b55..2aaf678 100644
--- a/panel.go
+++ b/panel.go
@@ -1,11 +1,11 @@
package tg
type Rowser interface {
- MakeRows(c *Context) []ButtonRow
+ MakeRows(c Context) []ButtonRow
}
-type RowserFunc func(c *Context) []ButtonRow
-func (fn RowserFunc) MakeRows(c *Context) []ButtonRow {
+type RowserFunc func(c Context) []ButtonRow
+func (fn RowserFunc) MakeRows(c Context) []ButtonRow {
return fn(c)
}
@@ -14,26 +14,26 @@ func (fn RowserFunc) MakeRows(c *Context) []ButtonRow {
// Can be used for example to show users via SQL and offset
// or something like that.
type PanelCompo struct {
- *InlineCompo
+ InlineCompo
Rowser Rowser
}
// Transform to the panel with dynamic rows.
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.
) *PanelCompo {
ret := &PanelCompo{}
- ret.InlineCompo = compo.Inline(
+ ret.InlineCompo = (*compo.Inline(
NewKeyboard(
rowser.MakeRows(c)...,
).Inline(),
- )
+ ))
ret.Rowser = rowser
return ret
}
-func (compo *PanelCompo) Update(c *Context) {
+func (compo *PanelCompo) Update(c Context) {
compo.Rows = compo.Rowser.MakeRows(c)
compo.InlineCompo.Update(c)
}
diff --git a/reply.go b/reply.go
index 06a470a..1b42ec1 100644
--- a/reply.go
+++ b/reply.go
@@ -41,7 +41,7 @@ func (kbd Reply) ToApi() any {
}
buttons := []tgbotapi.KeyboardButton{}
for _, button := range row {
- if button == nil {
+ if !button.Valid {
continue
}
buttons = append(buttons, button.ToTelegram())
@@ -63,17 +63,21 @@ type ReplyCompo struct {
}
// Implementing the sendable interface.
-func (compo ReplyCompo) SendConfig(
+func (compo *ReplyCompo) SendConfig(
sid SessionId, bot *Bot,
) (SendConfig) {
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
}
// Implementing the Server interface.
-func (compo ReplyCompo) Filter(
- u *Update,
+func (compo *ReplyCompo) Filter(
+ u Update,
) bool {
if compo == nil || u.Message == nil {
return true
@@ -82,8 +86,8 @@ func (compo ReplyCompo) Filter(
_, ok := compo.ButtonMap()[u.Message.Text]
if !ok {
if u.Message.Location != nil {
- locBtn := compo.ButtonMap().LocationButton()
- if locBtn == nil {
+ _, hasLocBtn := compo.ButtonMap().LocationButton()
+ if !hasLocBtn {
return true
}
} else {
@@ -94,22 +98,27 @@ func (compo ReplyCompo) Filter(
}
// Implementing the UI interface.
-func (compo ReplyCompo) Serve(c *Context) {
+func (compo *ReplyCompo) Serve(c Context) {
for u := range c.Input() {
- var btn *Button
+ var btn Button
text := u.Message.Text
btns := compo.ButtonMap()
btn, ok := btns[text]
if !ok {
if u.Message.Location != nil {
- btn = btns.LocationButton()
+ locBtn, hasLocBtn := btns.LocationButton()
+ if hasLocBtn {
+ btn = locBtn
+ }
}
}
- if btn != nil {
- c.WithUpdate(u).Run(btn.Action)
+ if !btn.Valid {
+ continue
}
+
+ c.WithUpdate(u).Run(btn.Action)
}
}
diff --git a/screen.go b/screen.go
index bafb3ba..674180a 100644
--- a/screen.go
+++ b/screen.go
@@ -7,15 +7,15 @@ import (
// The type implements changing screen to the underlying ScreenId
type ScreenGo struct {
Path Path
- Args []any
+ Arg any
}
-func (sc ScreenGo) Act(c *Context) {
- c.Go(sc.Path, sc.Args...)
+func (sc ScreenGo) Act(c Context) {
+ c.GoWithArg(sc.Path, sc.Arg)
}
// The same as Act.
-func (sc ScreenGo) Serve(c *Context) {
+func (sc ScreenGo) Serve(c Context) {
sc.Act(c)
}
diff --git a/send.go b/send.go
index 48cdc5d..7403286 100644
--- a/send.go
+++ b/send.go
@@ -11,7 +11,7 @@ type MessageId int64
// sent to the side of a user.
type Sendable interface {
SendConfig(SessionId, *Bot) (SendConfig)
- SetMessage(Message)
+ SetMessage(*Message)
}
// The type is used as an endpoint to send messages
diff --git a/server.go b/server.go
index 4d36063..ce782a7 100644
--- a/server.go
+++ b/server.go
@@ -3,6 +3,6 @@ package tg
// Implementing the interface provides
// the way to define how to handle updates.
type Server interface {
- Serve(*Context)
+ Serve(Context)
}
diff --git a/session.go b/session.go
index 1c554f5..e8476fe 100644
--- a/session.go
+++ b/session.go
@@ -5,8 +5,12 @@ package tg
type SessionMap map[SessionId]*Session
// Add new empty session by it's ID.
-func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session {
- ret := NewSession(sid, scope)
+func (sm SessionMap) Add(
+ bot *Bot,
+ sid SessionId,
+ scope SessionScope,
+) *Session {
+ ret := NewSession(bot, sid, scope)
sm[sid] = ret
return ret
}
@@ -45,137 +49,15 @@ type Session struct {
updates *UpdateChan
}
-// Return new empty session with specified user ID.
-func NewSession(id SessionId, scope SessionScope) *Session {
- return &Session{
- Id: id,
- Scope: scope,
- }
+// Return new empty session.
+func NewSession(bot *Bot, id SessionId, scope SessionScope) *Session {
+ ret := &Session{}
+ ret.Id = id
+ 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
-}
diff --git a/taskfile.yml b/taskfile.yml
deleted file mode 100644
index a86fbd8..0000000
--- a/taskfile.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 3
-
-tasks:
- build:
- cmds:
- - go build -o testbot ./cmd/test/
diff --git a/ui.go b/ui.go
index d952255..2a89ab4 100644
--- a/ui.go
+++ b/ui.go
@@ -3,12 +3,12 @@ package tg
// The type describes dynamic screen widget
// That can have multiple UI components.
type Widget interface {
- Render(*Context) UI
+ Render(Context) UI
}
// The way to describe custom function based Widgets.
-type RenderFunc func(c *Context) UI
-func (fn RenderFunc) Render(c *Context) UI {
+type RenderFunc func(c Context) UI
+func (fn RenderFunc) Render(c Context) UI {
return fn(c)
}
diff --git a/update.go b/update.go
index 219ddd2..cfd3122 100644
--- a/update.go
+++ b/update.go
@@ -16,18 +16,18 @@ type UpdateChan struct {
// Return new update channel.
func NewUpdateChan() *UpdateChan {
ret := &UpdateChan{}
- ret.chn = make(chan *Update)
+ ret.chn = make(chan Update)
return ret
}
-func (updates *UpdateChan) Chan() chan *Update {
+func (updates *UpdateChan) Chan() chan Update {
return updates.chn
}
// Send an update to the channel.
// Returns true if the update was sent.
-func (updates *UpdateChan) Send(u *Update) bool {
+func (updates *UpdateChan) Send(u Update) bool {
defer recover()
if updates == nil || updates.chn == nil {
return false
@@ -37,11 +37,11 @@ func (updates *UpdateChan) Send(u *Update) bool {
}
// Read an update from the channel.
-func (updates *UpdateChan) Read() *Update {
+func (updates *UpdateChan) Read() (Update, bool) {
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.
@@ -60,8 +60,7 @@ func (updates *UpdateChan) Close() {
}
func (u Update) HasDocument() bool {
- return u != nil &&
- u.Message != nil &&
+ return u.Message != nil &&
u.Message.Document != nil
}