Skip to content

Commit dd6db6c

Browse files
committed
docs: add comprehensive multi-file demo example bot
Add examples/demo, a single bot exercising every major botapi feature split across focused files: lifecycle/options/storage, commands and formatting, inline + reply keyboards and callbacks, media in both directions, rich and plain-text draft streaming, inline mode, a group-scoped admin command set, free-text predicates and middleware.
1 parent 44a4b37 commit dd6db6c

11 files changed

Lines changed: 1033 additions & 0 deletions

File tree

examples/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ APP_ID=12345 APP_HASH=abcdef BOT_TOKEN=123:abc go run ./examples/echo
99
Bots need an MTProto app identity (`APP_ID`/`APP_HASH`, from
1010
<https://my.telegram.org>) plus a BotFather token (`BOT_TOKEN`).
1111

12+
## demo — the full tour
13+
14+
[`demo`](./demo) is the comprehensive one: a single bot that exercises every
15+
major feature, split across focused files so each subsystem reads on its own —
16+
`main.go` (wiring & lifecycle), `commands.go` (formatting, content, editing),
17+
`keyboards.go` (inline + reply keyboards and callbacks), `media.go` (sending and
18+
receiving media), `inline.go` (inline mode), `admin.go` (a group-scoped command
19+
set: chat info, reactions, pinning, the raw escape hatch), `text.go` (free-text
20+
predicates and edits) and shared `middleware.go`/`helpers.go`. Enable inline mode
21+
in @BotFather to try the inline queries.
22+
23+
```bash
24+
APP_ID=12345 APP_HASH=abcdef BOT_TOKEN=123:abc go run ./examples/demo
25+
```
26+
27+
The other directories are smaller, single-feature bots.
28+
1229
## Logging
1330

1431
The bots log structured **JSONL** via zap. The shared

examples/demo/admin.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
glog "github.com/gotd/log"
7+
8+
"github.com/gotd/botapi"
9+
)
10+
11+
// registerAdmin builds a Group whose handlers only run in group/supergroup chats
12+
// and share an extra middleware. Groups keep related, similarly-gated handlers
13+
// together instead of repeating the same predicate on each registration.
14+
func registerAdmin(bot *botapi.Bot) {
15+
// The group predicate narrows every handler below to group chats; Use adds a
16+
// middleware that runs only for this group.
17+
groups := bot.Group(botapi.Or(
18+
botapi.ChatTypeIs(botapi.ChatTypeGroup),
19+
botapi.ChatTypeIs(botapi.ChatTypeSupergroup),
20+
)).Use(announce())
21+
22+
// Chat info: title, members count and the caller's membership status.
23+
groups.OnCommand("info", "Show info about this group", func(c *botapi.Context) error {
24+
chat, _ := c.Chat()
25+
26+
info, err := c.Bot.GetChat(c, chat)
27+
if err != nil {
28+
return err
29+
}
30+
31+
count, err := c.Bot.GetChatMemberCount(c, chat)
32+
if err != nil {
33+
return err
34+
}
35+
36+
status := "unknown"
37+
38+
if u := c.Sender(); u != nil {
39+
if member, err := c.Bot.GetChatMember(c, chat, u.ID); err == nil {
40+
status = chatMemberStatus(member)
41+
}
42+
}
43+
44+
_, err = c.Reply(fmt.Sprintf("Group %q\nMembers: %d\nYour status: %s",
45+
info.Title, count, status))
46+
47+
return err
48+
})
49+
50+
// React to the command message with an emoji, then pin it.
51+
groups.OnCommand("pin", "React to and pin your command", func(c *botapi.Context) error {
52+
chat, _ := c.Chat()
53+
id := c.Message().MessageID
54+
55+
if err := c.Bot.SetMessageReaction(c, chat, id, []botapi.ReactionType{botapi.Emoji("👍")}); err != nil {
56+
return err
57+
}
58+
59+
if err := c.Bot.PinChatMessage(c, chat, id, botapi.Silent()); err != nil {
60+
return err
61+
}
62+
63+
_, err := c.Reply("📌 Pinned (and reacted).")
64+
65+
return err
66+
})
67+
68+
// Raw() is the escape hatch to the underlying *tg.Client for anything the
69+
// typed surface doesn't cover. Here we just confirm it's reachable.
70+
groups.OnCommand("raw", "Demonstrate the raw MTProto client escape hatch", func(c *botapi.Context) error {
71+
_ = c.Bot.Raw() // *tg.Client — full MTProto API available here.
72+
73+
_, err := c.Reply("The raw *tg.Client is reachable via Bot.Raw() for anything not yet typed.")
74+
75+
return err
76+
})
77+
}
78+
79+
// announce is a group-scoped middleware: it logs every group-chat update it
80+
// handles before delegating. Unlike the global metrics middleware it is attached
81+
// only to this Group, so it runs for group commands only.
82+
func announce() botapi.Middleware {
83+
return func(next botapi.Handler) botapi.Handler {
84+
return func(c *botapi.Context) error {
85+
glog.For(c.Bot.Logger()).Debug(c, "group command", glog.String("text", c.Message().Text))
86+
87+
return next(c)
88+
}
89+
}
90+
}
91+
92+
// chatMemberStatus extracts the status string from a ChatMember union value.
93+
func chatMemberStatus(m botapi.ChatMember) string {
94+
switch v := m.(type) {
95+
case *botapi.ChatMemberOwner:
96+
return string(v.Status)
97+
case *botapi.ChatMemberAdministrator:
98+
return string(v.Status)
99+
case *botapi.ChatMemberMember:
100+
return string(v.Status)
101+
case *botapi.ChatMemberRestricted:
102+
return string(v.Status)
103+
case *botapi.ChatMemberLeft:
104+
return string(v.Status)
105+
case *botapi.ChatMemberBanned:
106+
return string(v.Status)
107+
default:
108+
return "unknown"
109+
}
110+
}

examples/demo/commands.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
glog "github.com/gotd/log"
8+
"github.com/gotd/td/telegram/message/rich"
9+
10+
"github.com/gotd/botapi"
11+
)
12+
13+
// registerCommands wires the slash commands. Each (name, description) pair is
14+
// published to Telegram's command menu via SetMyCommands when the bot starts.
15+
func registerCommands(bot *botapi.Bot) {
16+
bot.OnCommand("start", "Welcome message and reply keyboard", func(c *botapi.Context) error {
17+
_, err := c.Reply(
18+
"👋 Welcome to the gotd/botapi demo!\nType /help to see everything I can do.",
19+
botapi.WithReplyMarkup(mainReplyKeyboard()),
20+
)
21+
22+
return err
23+
})
24+
25+
bot.OnCommand("help", "List the available demos", func(c *botapi.Context) error {
26+
_, err := c.Reply(helpText,
27+
botapi.WithParseMode(botapi.ParseModeHTML),
28+
botapi.DisableWebPagePreview(),
29+
)
30+
31+
return err
32+
})
33+
34+
registerFormatting(bot)
35+
registerContent(bot)
36+
registerEditing(bot)
37+
}
38+
39+
// registerFormatting shows the three text-formatting routes: parse modes and
40+
// the structured rich-message API.
41+
func registerFormatting(bot *botapi.Bot) {
42+
bot.OnCommand("html", "Send an HTML-formatted message", func(c *botapi.Context) error {
43+
_, err := c.Reply(
44+
`<b>bold</b>, <i>italic</i>, <code>code</code>, <a href="https://telegram.org">link</a>, <tg-spoiler>spoiler</tg-spoiler>`,
45+
botapi.WithParseMode(botapi.ParseModeHTML),
46+
)
47+
48+
return err
49+
})
50+
51+
bot.OnCommand("md", "Send a MarkdownV2-formatted message", func(c *botapi.Context) error {
52+
_, err := c.Reply(
53+
"*bold*, _italic_, `code`, ||spoiler||",
54+
botapi.WithParseMode(botapi.ParseModeMarkdownV2),
55+
)
56+
57+
return err
58+
})
59+
60+
// A rich message (Bot API 10.1): a tree of page blocks rather than a flat
61+
// string, built with the gotd/td rich package and sent via SendRichMessage.
62+
bot.OnCommand("rich", "Send a structured rich message", func(c *botapi.Context) error {
63+
chat, _ := c.Chat()
64+
msg := rich.New(
65+
rich.Heading1(rich.Plain("Rich messages")),
66+
rich.Paragraph(rich.Concat(
67+
rich.Plain("They carry "),
68+
rich.Bold(rich.Plain("headings")),
69+
rich.Plain(", "),
70+
rich.Italic(rich.Plain("paragraphs")),
71+
rich.Plain(" and more."),
72+
)),
73+
rich.Preformatted(rich.Plain("bot.SendRichMessage(ctx, chat, msg)"), "go"),
74+
).Input()
75+
76+
_, err := c.Bot.SendRichMessage(c, chat, msg)
77+
78+
return err
79+
})
80+
}
81+
82+
// registerContent covers the non-text content send methods.
83+
func registerContent(bot *botapi.Bot) {
84+
bot.OnCommand("poll", "Send a poll", func(c *botapi.Context) error {
85+
chat, _ := c.Chat()
86+
_, err := c.Bot.SendPoll(c, chat, "Best way to build a Telegram bot?",
87+
[]string{"MTProto (gotd)", "HTTP Bot API", "Both"})
88+
89+
return err
90+
})
91+
92+
bot.OnCommand("dice", "Roll a dice", func(c *botapi.Context) error {
93+
chat, _ := c.Chat()
94+
_, err := c.Bot.SendDice(c, chat, botapi.DiceDie)
95+
96+
return err
97+
})
98+
99+
bot.OnCommand("location", "Send a location", func(c *botapi.Context) error {
100+
chat, _ := c.Chat()
101+
_, err := c.Bot.SendLocation(c, chat, 55.7558, 37.6173)
102+
103+
return err
104+
})
105+
106+
bot.OnCommand("venue", "Send a venue", func(c *botapi.Context) error {
107+
chat, _ := c.Chat()
108+
_, err := c.Bot.SendVenue(c, chat, 55.7520, 37.6175, "Red Square", "Moscow, Russia")
109+
110+
return err
111+
})
112+
113+
bot.OnCommand("contact", "Send a contact", func(c *botapi.Context) error {
114+
chat, _ := c.Chat()
115+
_, err := c.Bot.SendContact(c, chat, "+1234567890", "Ada", "Lovelace")
116+
117+
return err
118+
})
119+
120+
// Send options: a message with no notification, and one whose content can't
121+
// be forwarded or saved.
122+
bot.OnCommand("silent", "Send a message without a notification", func(c *botapi.Context) error {
123+
_, err := c.Reply("🔕 Sent silently.", botapi.Silent())
124+
return err
125+
})
126+
127+
bot.OnCommand("protect", "Send a message that can't be forwarded", func(c *botapi.Context) error {
128+
_, err := c.Reply("🔒 This message has protected content.", botapi.ProtectContent())
129+
return err
130+
})
131+
132+
// Background send: reply now, then send again from a goroutine that outlives
133+
// the handler. The handler's own context is canceled on return, so proactive
134+
// work must use the bot's run-lifetime Background context.
135+
bot.OnCommand("remind", "Send a reminder in 5 seconds (background send)", func(c *botapi.Context) error {
136+
chat, _ := c.Chat()
137+
ctx := c.Background()
138+
139+
go func() {
140+
time.Sleep(5 * time.Second)
141+
142+
if _, err := c.Bot.SendMessage(ctx, chat, "⏰ Here's your reminder!"); err != nil {
143+
glog.For(c.Bot.Logger()).Warn(ctx, "background reminder failed", glog.Error(err))
144+
}
145+
}()
146+
147+
_, err := c.Reply("OK, I'll remind you in 5 seconds.")
148+
149+
return err
150+
})
151+
}
152+
153+
// registerEditing demonstrates the edit, forward and delete methods as a single
154+
// self-narrating sequence.
155+
func registerEditing(bot *botapi.Bot) {
156+
bot.OnCommand("edit", "Send a message, then edit its text and markup", func(c *botapi.Context) error {
157+
chat, _ := c.Chat()
158+
159+
m, err := c.Bot.SendMessage(c, chat, "Step 0: this message will change…")
160+
if err != nil {
161+
return err
162+
}
163+
164+
time.Sleep(time.Second)
165+
166+
if _, err := c.Bot.EditMessageText(c, chat, m.MessageID, "Step 1: ✏️ text edited"); err != nil {
167+
return err
168+
}
169+
170+
time.Sleep(time.Second)
171+
172+
kb := botapi.InlineKeyboard(botapi.InlineRow(
173+
botapi.InlineButtonData("✅ markup edited", "demo:ok"),
174+
))
175+
176+
_, err = c.Bot.EditMessageText(c, chat, m.MessageID,
177+
"Step 2: ✅ all edits succeeded", botapi.WithReplyMarkup(kb))
178+
179+
return err
180+
})
181+
182+
bot.OnCommand("forward", "Forward your command back to you", func(c *botapi.Context) error {
183+
chat, _ := c.Chat()
184+
_, err := c.Bot.ForwardMessage(c, chat, chat, c.Message().MessageID)
185+
186+
return err
187+
})
188+
189+
// Send a throwaway message and delete it a moment later.
190+
bot.OnCommand("selfdestruct", "Send a message that deletes itself", func(c *botapi.Context) error {
191+
chat, _ := c.Chat()
192+
193+
m, err := c.Bot.SendMessage(c, chat, "💣 This message self-destructs in 3 seconds…")
194+
if err != nil {
195+
return err
196+
}
197+
198+
ctx := c.Background()
199+
200+
go func() {
201+
time.Sleep(3 * time.Second)
202+
203+
if err := c.Bot.DeleteMessage(ctx, chat, m.MessageID); err != nil {
204+
glog.For(c.Bot.Logger()).Warn(ctx, "self-destruct delete failed", glog.Error(err))
205+
}
206+
}()
207+
208+
return nil
209+
})
210+
211+
// PeerRef turns the current chat into a small, JSON-serializable reference you
212+
// can persist and later send to with botapi.Peer(ref) — no re-resolution.
213+
bot.OnCommand("ref", "Show this chat's serializable PeerRef", func(c *botapi.Context) error {
214+
chat, _ := c.Chat()
215+
216+
ref, err := c.Bot.PeerRef(c, chat)
217+
if err != nil {
218+
return err
219+
}
220+
221+
_, err = c.Reply(fmt.Sprintf("PeerRef{Kind: %q, ID: %d, AccessHash: %d}",
222+
ref.Kind, ref.ID, ref.AccessHash))
223+
224+
return err
225+
})
226+
}

0 commit comments

Comments
 (0)