Skip to content

Commit 5a8bcab

Browse files
ernadoclaude
andcommitted
docs(examples): add advanced full-featured demo bot
A single bot exercising most of the surface: auto-registered commands, HTML/MarkdownV2 formatting, inline and reply keyboards, callback queries, inline mode, the media sends (photo/poll/dice/location/venue/contact), incoming-media handling (photo/document/sticker/location/contact), editing, forwarding, chat actions, edited-message and regex handlers, and the recover/timeout/logging middleware. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a90a3fa commit 5a8bcab

1 file changed

Lines changed: 369 additions & 0 deletions

File tree

examples/advanced/main.go

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
// Command advanced is a full-featured demo bot that exercises most of the
2+
// github.com/gotd/botapi surface: commands (auto-registered), formatting,
3+
// inline and reply keyboards, callback queries, inline mode, the media send
4+
// methods, incoming-media handling, editing, forwarding, chat actions, polls,
5+
// dice, location/venue/contact and the predicate/middleware framework.
6+
//
7+
// Run it with an MTProto app identity (https://my.telegram.org) and a BotFather
8+
// token (enable inline mode in @BotFather to test inline queries):
9+
//
10+
// APP_ID=12345 APP_HASH=abcdef BOT_TOKEN=123:abc go run ./examples/advanced
11+
package main
12+
13+
import (
14+
"context"
15+
"fmt"
16+
"os"
17+
"os/signal"
18+
"strconv"
19+
"strings"
20+
"time"
21+
22+
"go.uber.org/zap"
23+
24+
"github.com/gotd/botapi"
25+
)
26+
27+
const samplePhotoURL = "https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg"
28+
29+
func main() {
30+
log, _ := zap.NewDevelopment()
31+
defer func() { _ = log.Sync() }()
32+
33+
appID, err := strconv.Atoi(os.Getenv("APP_ID"))
34+
if err != nil {
35+
log.Fatal("APP_ID must be a number (see https://my.telegram.org)", zap.Error(err))
36+
}
37+
38+
bot, err := botapi.New(os.Getenv("BOT_TOKEN"), botapi.Options{
39+
AppID: appID,
40+
AppHash: os.Getenv("APP_HASH"),
41+
Logger: log,
42+
FloodWait: true, // transparently wait out flood limits
43+
})
44+
if err != nil {
45+
log.Fatal("Create bot", zap.Error(err))
46+
}
47+
48+
// Global middleware: recover from panics, bound each handler, log outcomes.
49+
bot.Use(botapi.Recover(), botapi.Timeout(time.Minute), botapi.Logging())
50+
51+
registerCommands(bot)
52+
registerKeyboards(bot)
53+
registerMedia(bot)
54+
registerMisc(bot)
55+
56+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
57+
defer cancel()
58+
59+
log.Info("Starting advanced bot")
60+
if err := bot.Run(ctx); err != nil {
61+
log.Fatal("Run", zap.Error(err))
62+
}
63+
}
64+
65+
// registerCommands wires the slash commands. Each description is published to
66+
// the client command menu via SetMyCommands on start.
67+
func registerCommands(bot *botapi.Bot) {
68+
bot.OnCommand("start", "Welcome message and reply keyboard", func(c *botapi.Context) error {
69+
_, err := c.Reply("👋 Welcome! Type /help to see everything I can do.",
70+
botapi.WithReplyMarkup(mainReplyKeyboard()))
71+
return err
72+
})
73+
74+
bot.OnCommand("help", "List the available demos", func(c *botapi.Context) error {
75+
_, err := c.Reply(helpText, botapi.WithParseMode(botapi.ParseModeHTML), botapi.DisableWebPagePreview())
76+
return err
77+
})
78+
79+
bot.OnCommand("html", "Send an HTML-formatted message", func(c *botapi.Context) error {
80+
_, err := c.Reply("<b>bold</b>, <i>italic</i>, <code>code</code>, <a href=\"https://telegram.org\">link</a>",
81+
botapi.WithParseMode(botapi.ParseModeHTML))
82+
return err
83+
})
84+
85+
bot.OnCommand("md", "Send a MarkdownV2-formatted message", func(c *botapi.Context) error {
86+
_, err := c.Reply("*bold*, _italic_, `code`, ||spoiler||",
87+
botapi.WithParseMode(botapi.ParseModeMarkdownV2))
88+
return err
89+
})
90+
91+
bot.OnCommand("keyboard", "Show an inline keyboard with callbacks", func(c *botapi.Context) error {
92+
_, err := c.Reply("Pick one:", botapi.WithReplyMarkup(demoInlineKeyboard()))
93+
return err
94+
})
95+
96+
bot.OnCommand("poll", "Send a poll", func(c *botapi.Context) error {
97+
chat, _ := c.Chat()
98+
_, err := c.Bot.SendPoll(c, chat, "Best Go web framework?",
99+
[]string{"net/http", "gin", "echo", "fiber"})
100+
return err
101+
})
102+
103+
bot.OnCommand("dice", "Roll a dice", func(c *botapi.Context) error {
104+
chat, _ := c.Chat()
105+
_, err := c.Bot.SendDice(c, chat, botapi.DiceDie)
106+
return err
107+
})
108+
109+
bot.OnCommand("location", "Send a location", func(c *botapi.Context) error {
110+
chat, _ := c.Chat()
111+
_, err := c.Bot.SendLocation(c, chat, 55.7558, 37.6173)
112+
return err
113+
})
114+
115+
bot.OnCommand("venue", "Send a venue", func(c *botapi.Context) error {
116+
chat, _ := c.Chat()
117+
_, err := c.Bot.SendVenue(c, chat, 55.7520, 37.6175, "Red Square", "Moscow, Russia")
118+
return err
119+
})
120+
121+
bot.OnCommand("contact", "Send a contact", func(c *botapi.Context) error {
122+
chat, _ := c.Chat()
123+
_, err := c.Bot.SendContact(c, chat, "+1234567890", "Ada", "Lovelace")
124+
return err
125+
})
126+
127+
bot.OnCommand("photo", "Send a photo by URL", func(c *botapi.Context) error {
128+
chat, _ := c.Chat()
129+
_, err := c.Bot.SendPhoto(c, chat, botapi.FileURL(samplePhotoURL), "A cat 🐈 sent by URL")
130+
return err
131+
})
132+
133+
bot.OnCommand("typing", "Show a chat action, then reply", func(c *botapi.Context) error {
134+
chat, _ := c.Chat()
135+
if err := c.Bot.SendChatAction(c, chat, botapi.ChatActionTyping); err != nil {
136+
return err
137+
}
138+
time.Sleep(2 * time.Second)
139+
_, err := c.Reply("Done typing!")
140+
return err
141+
})
142+
143+
bot.OnCommand("edit", "Send a message, then edit it", func(c *botapi.Context) error {
144+
chat, _ := c.Chat()
145+
m, err := c.Bot.SendMessage(c, chat, "This message will change in 2 seconds…")
146+
if err != nil {
147+
return err
148+
}
149+
time.Sleep(2 * time.Second)
150+
_, err = c.Bot.EditMessageText(c, chat, m.MessageID, "✏️ Edited!")
151+
return err
152+
})
153+
154+
bot.OnCommand("forward", "Forward your command back to you", func(c *botapi.Context) error {
155+
chat, _ := c.Chat()
156+
_, err := c.Bot.ForwardMessage(c, chat, chat, c.Message().MessageID)
157+
return err
158+
})
159+
160+
bot.OnCommand("silent", "Send a message without a notification", func(c *botapi.Context) error {
161+
_, err := c.Reply("🔕 Sent silently.", botapi.Silent())
162+
return err
163+
})
164+
165+
bot.OnCommand("protect", "Send a message that can't be forwarded", func(c *botapi.Context) error {
166+
_, err := c.Reply("🔒 This message has protected content.", botapi.ProtectContent())
167+
return err
168+
})
169+
}
170+
171+
// registerKeyboards handles reply-keyboard interactions and callback queries.
172+
func registerKeyboards(bot *botapi.Bot) {
173+
bot.OnCommand("removekbd", "Remove the reply keyboard", func(c *botapi.Context) error {
174+
_, err := c.Reply("Keyboard removed.", botapi.WithReplyMarkup(&botapi.ReplyKeyboardRemove{RemoveKeyboard: true}))
175+
return err
176+
})
177+
178+
// Reply-keyboard buttons arrive as plain text messages.
179+
bot.OnMessage(func(c *botapi.Context) error {
180+
_, err := c.Reply("You pressed: " + c.Message().Text)
181+
return err
182+
}, botapi.Or(botapi.TextEquals("📊 Poll"), botapi.TextEquals("🎲 Dice"), botapi.TextEquals("📍 Location")))
183+
184+
// Inline-keyboard callbacks (prefix "demo:").
185+
bot.OnCallbackQuery(func(c *botapi.Context) error {
186+
data := strings.TrimPrefix(c.Update.CallbackQuery.Data, "demo:")
187+
if err := c.AnswerCallback(botapi.WithCallbackText("You chose " + data)); err != nil {
188+
return err
189+
}
190+
msg := c.Update.CallbackQuery.Message
191+
if msg == nil {
192+
return nil
193+
}
194+
_, err := c.Bot.EditMessageText(c, botapi.ID(msg.Chat.ID), msg.MessageID,
195+
"You chose: "+data, botapi.WithReplyMarkup(demoInlineKeyboard()))
196+
return err
197+
}, botapi.CallbackPrefix("demo:"))
198+
}
199+
200+
// registerMedia echoes incoming media and reports details.
201+
func registerMedia(bot *botapi.Bot) {
202+
bot.OnMessage(func(c *botapi.Context) error {
203+
photos := c.Message().Photo
204+
largest := photos[len(photos)-1]
205+
chat, _ := c.Chat()
206+
_, err := c.Bot.SendPhoto(c, chat, botapi.FileID(largest.FileID),
207+
fmt.Sprintf("Got your photo: %d×%d", largest.Width, largest.Height))
208+
return err
209+
}, hasPhoto)
210+
211+
bot.OnMessage(func(c *botapi.Context) error {
212+
doc := c.Message().Document
213+
file, err := c.Bot.GetFile(c, doc.FileID)
214+
if err != nil {
215+
return err
216+
}
217+
_, err = c.Reply(fmt.Sprintf("Got document %q (%d bytes)\nfile_unique_id=%s",
218+
doc.FileName, doc.FileSize, file.FileUniqueID))
219+
return err
220+
}, hasDocument)
221+
222+
bot.OnMessage(func(c *botapi.Context) error {
223+
s := c.Message().Sticker
224+
_, err := c.Reply("Nice sticker " + s.Emoji)
225+
return err
226+
}, hasSticker)
227+
228+
bot.OnMessage(func(c *botapi.Context) error {
229+
loc := c.Message().Location
230+
_, err := c.Reply(fmt.Sprintf("You are at %.4f, %.4f", loc.Latitude, loc.Longitude))
231+
return err
232+
}, hasLocation)
233+
234+
bot.OnMessage(func(c *botapi.Context) error {
235+
ct := c.Message().Contact
236+
_, err := c.Reply("Contact: " + ct.FirstName + " " + ct.PhoneNumber)
237+
return err
238+
}, hasContact)
239+
}
240+
241+
// registerMisc wires inline mode, edited-message handling and a text predicate.
242+
func registerMisc(bot *botapi.Bot) {
243+
// Inline mode: type "@yourbot something" in any chat.
244+
bot.OnInlineQuery(func(c *botapi.Context) error {
245+
q := strings.TrimSpace(c.Update.InlineQuery.Query)
246+
if q == "" {
247+
return c.AnswerInline(nil)
248+
}
249+
results := []botapi.InlineQueryResult{
250+
article("upper", "UPPERCASE", strings.ToUpper(q)),
251+
article("lower", "lowercase", strings.ToLower(q)),
252+
article("reverse", "Reversed", reverse(q)),
253+
}
254+
return c.AnswerInline(results, botapi.WithInlineCacheTime(1))
255+
})
256+
257+
bot.OnEditedMessage(func(c *botapi.Context) error {
258+
_, err := c.Reply("👀 I noticed you edited a message.")
259+
return err
260+
})
261+
262+
// Greet on "hi"/"hello"/"hey" (case-insensitive), but not commands.
263+
bot.OnMessage(func(c *botapi.Context) error {
264+
_, err := c.Reply("Hello, " + displayName(c.Sender()) + "! 👋")
265+
return err
266+
}, botapi.Regex(`(?i)^(hi|hello|hey)\b`))
267+
}
268+
269+
// --- helpers ---
270+
271+
func mainReplyKeyboard() *botapi.ReplyKeyboardMarkup {
272+
return &botapi.ReplyKeyboardMarkup{
273+
Keyboard: [][]botapi.KeyboardButton{
274+
{botapi.Button("📊 Poll"), botapi.Button("🎲 Dice")},
275+
{botapi.ButtonLocation("📍 Location"), botapi.ButtonContact("📇 Contact")},
276+
},
277+
ResizeKeyboard: true,
278+
}
279+
}
280+
281+
func demoInlineKeyboard() *botapi.InlineKeyboardMarkup {
282+
return botapi.InlineKeyboard(
283+
[]botapi.InlineKeyboardButton{
284+
botapi.InlineButtonData("🔴 Red", "demo:red"),
285+
botapi.InlineButtonData("🟢 Green", "demo:green"),
286+
botapi.InlineButtonData("🔵 Blue", "demo:blue"),
287+
},
288+
[]botapi.InlineKeyboardButton{
289+
botapi.InlineButtonURL("gotd/td", "https://github.com/gotd/td"),
290+
},
291+
)
292+
}
293+
294+
func article(id, title, text string) botapi.InlineQueryResult {
295+
return &botapi.InlineQueryResultArticle{
296+
ID: id,
297+
Title: title,
298+
Description: text,
299+
InputMessageContent: &botapi.InputTextMessageContent{MessageText: text},
300+
}
301+
}
302+
303+
func displayName(u *botapi.User) string {
304+
if u == nil {
305+
return "stranger"
306+
}
307+
if u.Username != "" {
308+
return "@" + u.Username
309+
}
310+
return u.FirstName
311+
}
312+
313+
func reverse(s string) string {
314+
r := []rune(s)
315+
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
316+
r[i], r[j] = r[j], r[i]
317+
}
318+
return string(r)
319+
}
320+
321+
func hasPhoto(u *botapi.Update) bool {
322+
m := u.EffectiveMessage()
323+
return m != nil && len(m.Photo) > 0
324+
}
325+
326+
func hasDocument(u *botapi.Update) bool {
327+
m := u.EffectiveMessage()
328+
return m != nil && m.Document != nil
329+
}
330+
331+
func hasSticker(u *botapi.Update) bool {
332+
m := u.EffectiveMessage()
333+
return m != nil && m.Sticker != nil
334+
}
335+
336+
func hasLocation(u *botapi.Update) bool {
337+
m := u.EffectiveMessage()
338+
return m != nil && m.Location != nil
339+
}
340+
341+
func hasContact(u *botapi.Update) bool {
342+
m := u.EffectiveMessage()
343+
return m != nil && m.Contact != nil
344+
}
345+
346+
const helpText = `<b>Advanced demo bot</b>
347+
348+
Commands:
349+
/html — HTML formatting
350+
/md — MarkdownV2 formatting
351+
/keyboard — inline keyboard + callbacks
352+
/removekbd — remove the reply keyboard
353+
/photo — send a photo by URL
354+
/poll — send a poll
355+
/dice — roll a dice
356+
/location — send a location
357+
/venue — send a venue
358+
/contact — send a contact
359+
/typing — chat action
360+
/edit — send then edit a message
361+
/forward — forward your message back
362+
/silent — silent message
363+
/protect — protected-content message
364+
365+
Also try:
366+
• send me a <i>photo</i>, <i>document</i>, <i>sticker</i>, <i>location</i> or <i>contact</i>
367+
• edit one of your messages
368+
• type <code>@thisbot hello</code> in any chat (inline mode)
369+
• say "hi"`

0 commit comments

Comments
 (0)