telegem 2.0.9 โ 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Readme.md +2 -2
- data/Starts_HallofFame.md +65 -0
- data/Test-Projects/bot.md +145 -0
- data/Test-Projects/bot1.rb +91 -0
- data/docs-src/Bot-registration_.PNG +0 -0
- data/docs-src/bot.md +349 -180
- data/docs-src/ctx.md +399 -0
- data/lib/api/client.rb +24 -23
- data/lib/core/bot.rb +69 -148
- data/lib/telegem.rb +1 -1
- data/lib/webhook/server.rb +87 -284
- data/setup.sh +30 -0
- metadata +21 -20
- data/Test-Projects/.gitkeep +0 -0
- data/Test-Projects/Movie-tracker-bot/.gitkeep +0 -0
- data/Test-Projects/Movie-tracker-bot/Gemfile +0 -2
- data/Test-Projects/Movie-tracker-bot/bot.rb +0 -62
- data/Test-Projects/Movie-tracker-bot/handlers/.gitkeep +0 -0
- data/Test-Projects/Movie-tracker-bot/handlers/add_1_.rb +0 -160
- data/Test-Projects/Movie-tracker-bot/handlers/add_2_.rb +0 -139
- data/Test-Projects/Movie-tracker-bot/handlers/premium.rb +0 -13
- data/Test-Projects/Movie-tracker-bot/handlers/report.rb +0 -31
- data/Test-Projects/Movie-tracker-bot/handlers/search.rb +0 -150
- data/Test-Projects/Movie-tracker-bot/handlers/sponsor.rb +0 -14
- data/Test-Projects/Movie-tracker-bot/handlers/start.rb +0 -48
- data/Test-Projects/Movie-tracker-bot/handlers/watch.rb +0 -210
- data/Test-Projects/Test-submitted-by-marvel/.gitkeep +0 -0
- data/Test-Projects/Test-submitted-by-marvel/Marvel-bot.md +0 -3
- data/Test-Projects/bot_test1.rb +0 -75
- data/Test-Projects/pizza_test_bot_guide.md +0 -163
- data/docs-src/understanding-ctx.md +0 -581
data/docs-src/ctx.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
.md - Your Gateway to Telegram Bot Mastery
|
|
2
|
+
|
|
3
|
+
๐ What is ctx?
|
|
4
|
+
|
|
5
|
+
Imagine you're at a coffee shop. The barista (ctx) is your connection to everything:
|
|
6
|
+
|
|
7
|
+
ยท Takes your order (message)
|
|
8
|
+
ยท Knows who you are (user info)
|
|
9
|
+
ยท Has your table number (chat info)
|
|
10
|
+
ยท Can bring you coffee (send replies)
|
|
11
|
+
ยท Remembers your usual (session data)
|
|
12
|
+
|
|
13
|
+
ctx is your barista for Telegram bots.
|
|
14
|
+
|
|
15
|
+
๐ฏ The 5 Essential Things ctx Gives You
|
|
16
|
+
|
|
17
|
+
1. Who's Talking? (ctx.from)
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# Every person has an ID card
|
|
21
|
+
user_id = ctx.from.id # Like a social security number (unique!)
|
|
22
|
+
username = ctx.from.username # @username (might be nil)
|
|
23
|
+
name = ctx.from.first_name # "John"
|
|
24
|
+
full_name = ctx.from.full_name # "John Doe" (first + last)
|
|
25
|
+
|
|
26
|
+
# Quick check:
|
|
27
|
+
if ctx.from.is_bot
|
|
28
|
+
ctx.reply("Hey fellow bot! ๐ค")
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Where Are We? (ctx.chat)
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
chat_id = ctx.chat.id # Room number
|
|
36
|
+
room_type = ctx.chat.type # "private", "group", "supergroup", "channel"
|
|
37
|
+
|
|
38
|
+
# Different rooms, different rules:
|
|
39
|
+
case ctx.chat.type
|
|
40
|
+
when "private"
|
|
41
|
+
ctx.reply("Just us talking! ๐คซ")
|
|
42
|
+
when "group"
|
|
43
|
+
ctx.reply("Hello everyone in the group! ๐")
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. What Was Said? (ctx.message)
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# The actual message
|
|
51
|
+
text = ctx.message.text # What they typed
|
|
52
|
+
msg_id = ctx.message.message_id # Message ID (for editing/deleting)
|
|
53
|
+
|
|
54
|
+
# Was it a photo?
|
|
55
|
+
if ctx.message.photo
|
|
56
|
+
ctx.reply("Nice photo! ๐ธ")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Was it a location?
|
|
60
|
+
if ctx.message.location
|
|
61
|
+
lat = ctx.message.location.latitude
|
|
62
|
+
lng = ctx.message.location.longitude
|
|
63
|
+
ctx.reply("You're at #{lat}, #{lng}")
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
4. Did They Click a Button? (ctx.data)
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# Only works for inline button clicks
|
|
71
|
+
bot.on(:callback_query) do |ctx|
|
|
72
|
+
# ctx.data contains what you put in callback_data
|
|
73
|
+
case ctx.data
|
|
74
|
+
when "pizza"
|
|
75
|
+
ctx.reply("๐ Pizza ordered!")
|
|
76
|
+
when "burger"
|
|
77
|
+
ctx.reply("๐ Burger coming up!")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ALWAYS answer callback queries!
|
|
81
|
+
ctx.answer_callback_query(text: "Done!")
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
5. Remember Stuff (ctx.session & ctx.state)
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# ctx.session = Long-term memory (survives restarts)
|
|
89
|
+
ctx.session[:language] = "en" # User prefers English
|
|
90
|
+
ctx.session[:pizza_count] ||= 0 # Start at 0, then increment
|
|
91
|
+
ctx.session[:pizza_count] += 1
|
|
92
|
+
|
|
93
|
+
# ctx.state = Short-term memory (current conversation)
|
|
94
|
+
ctx.state[:asking_for_name] = true # Just for this flow
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
๐ Your First 5 Minutes with ctx
|
|
98
|
+
|
|
99
|
+
Minute 1: Echo Bot
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
bot.on(:message) do |ctx|
|
|
103
|
+
# Whatever user says, repeat it back
|
|
104
|
+
ctx.reply("You said: #{ctx.message.text}")
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Minute 2: Welcome Bot
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
bot.command('start') do |ctx|
|
|
112
|
+
ctx.reply("Welcome #{ctx.from.first_name}! ๐")
|
|
113
|
+
ctx.reply("Your ID: #{ctx.from.id}")
|
|
114
|
+
ctx.reply("Chat ID: #{ctx.chat.id}")
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Minute 3: Memory Bot
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
bot.command('count') do |ctx|
|
|
122
|
+
# Count how many times user used /count
|
|
123
|
+
ctx.session[:count] ||= 0
|
|
124
|
+
ctx.session[:count] += 1
|
|
125
|
+
ctx.reply("You've counted #{ctx.session[:count]} times!")
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Minute 4: Smart Bot
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
bot.hears(/hello|hi|hey/i) do |ctx|
|
|
133
|
+
if ctx.chat.type == "private"
|
|
134
|
+
ctx.reply("Hello there! ๐")
|
|
135
|
+
else
|
|
136
|
+
ctx.reply("Hello #{ctx.from.first_name}! ๐")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Minute 5: Media Bot
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
bot.command('cat') do |ctx|
|
|
145
|
+
ctx.reply("Here's a cat! ๐ฑ")
|
|
146
|
+
ctx.photo("https://cataas.com/cat", caption: "Random cat!")
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
๐ฆ The ctx Toolbox (25+ Methods)
|
|
151
|
+
|
|
152
|
+
Sending Messages
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# Text messages
|
|
156
|
+
ctx.reply("Hello!") # Basic
|
|
157
|
+
ctx.reply("*Bold text*", parse_mode: "Markdown") # Formatted
|
|
158
|
+
ctx.reply("<b>HTML bold</b>", parse_mode: "HTML") # HTML
|
|
159
|
+
|
|
160
|
+
# Replying to specific message
|
|
161
|
+
ctx.reply("Answering this", reply_to_message_id: 123)
|
|
162
|
+
|
|
163
|
+
# With keyboard at bottom
|
|
164
|
+
keyboard = Telegem.keyboard { row "Yes", "No" }
|
|
165
|
+
ctx.reply("Choose:", reply_markup: keyboard)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Sending Files & Media
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# Photo (from URL, file, or file_id)
|
|
172
|
+
ctx.photo("https://example.com/cat.jpg")
|
|
173
|
+
ctx.photo(File.open("cat.jpg"))
|
|
174
|
+
ctx.photo("AgACAx...") # Telegram file_id
|
|
175
|
+
|
|
176
|
+
# With caption
|
|
177
|
+
ctx.photo("cat.jpg", caption: "My cat! ๐ฑ")
|
|
178
|
+
|
|
179
|
+
# Document (PDF, etc.)
|
|
180
|
+
ctx.document("report.pdf", caption: "Monthly report")
|
|
181
|
+
|
|
182
|
+
# Audio, Video, Voice
|
|
183
|
+
ctx.audio("song.mp3", caption: "My song")
|
|
184
|
+
ctx.video("clip.mp4", caption: "Funny video!")
|
|
185
|
+
ctx.voice("message.ogg", caption: "Voice note")
|
|
186
|
+
|
|
187
|
+
# Location
|
|
188
|
+
ctx.location(51.5074, -0.1278) # London coordinates
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Managing Messages
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
# Edit a message (need its message_id)
|
|
195
|
+
ctx.edit_message_text("Updated text!", message_id: 123)
|
|
196
|
+
|
|
197
|
+
# Delete messages
|
|
198
|
+
ctx.delete_message # Current message
|
|
199
|
+
ctx.delete_message(123) # Specific message
|
|
200
|
+
|
|
201
|
+
# Forward/Copy messages
|
|
202
|
+
ctx.forward_message(source_chat_id, message_id)
|
|
203
|
+
ctx.copy_message(source_chat_id, message_id)
|
|
204
|
+
|
|
205
|
+
# Pin/Unpin
|
|
206
|
+
ctx.pin_message(message_id)
|
|
207
|
+
ctx.unpin_message
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Interactive Features
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Show "typing..." indicator
|
|
214
|
+
ctx.typing
|
|
215
|
+
# or
|
|
216
|
+
ctx.with_typing do
|
|
217
|
+
# Long operation here
|
|
218
|
+
sleep 2
|
|
219
|
+
ctx.reply("Done thinking!")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Show other actions
|
|
223
|
+
ctx.uploading_photo # "uploading photo..."
|
|
224
|
+
ctx.uploading_document # "uploading document..."
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Group Management (Bot needs admin)
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
ctx.kick_chat_member(user_id) # Remove from group
|
|
231
|
+
ctx.ban_chat_member(user_id) # Ban user
|
|
232
|
+
ctx.unban_chat_member(user_id) # Unban user
|
|
233
|
+
|
|
234
|
+
# Get info
|
|
235
|
+
admins = ctx.get_chat_administrators
|
|
236
|
+
member_count = ctx.get_chat_members_count
|
|
237
|
+
chat_info = ctx.get_chat
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
๐ญ Real-World Scenarios
|
|
241
|
+
|
|
242
|
+
Scenario 1: Pizza Order
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
bot.command('order') do |ctx|
|
|
246
|
+
# Step 1: Ask for pizza type
|
|
247
|
+
keyboard = ctx.keyboard do
|
|
248
|
+
row "Margherita", "Pepperoni"
|
|
249
|
+
row "Veggie", "Cancel"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
ctx.reply("Choose pizza:", reply_markup: keyboard)
|
|
253
|
+
ctx.state[:step] = "waiting_for_pizza"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Handle the choice
|
|
257
|
+
bot.hears("Margherita") do |ctx|
|
|
258
|
+
if ctx.state[:step] == "waiting_for_pizza"
|
|
259
|
+
ctx.reply("๐ Margherita selected!")
|
|
260
|
+
ctx.reply("What's your address?")
|
|
261
|
+
ctx.state[:step] = "waiting_for_address"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Scenario 2: Quiz Game
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
bot.command('quiz') do |ctx|
|
|
270
|
+
ctx.session[:score] ||= 0
|
|
271
|
+
|
|
272
|
+
inline = ctx.inline_keyboard do
|
|
273
|
+
row button "Paris", callback_data: "answer_paris"
|
|
274
|
+
row button "London", callback_data: "answer_london"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
ctx.reply("Capital of France?", reply_markup: inline)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
bot.on(:callback_query) do |ctx|
|
|
281
|
+
if ctx.data == "answer_paris"
|
|
282
|
+
ctx.session[:score] += 1
|
|
283
|
+
ctx.answer_callback_query(text: "โ
Correct!")
|
|
284
|
+
ctx.edit_message_text("๐ Correct! Score: #{ctx.session[:score]}")
|
|
285
|
+
else
|
|
286
|
+
ctx.answer_callback_query(text: "โ Wrong!")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Scenario 3: Support Ticket
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
bot.command('support') do |ctx|
|
|
295
|
+
ctx.reply("Describe your issue:")
|
|
296
|
+
ctx.state[:collecting_issue] = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
bot.on(:message) do |ctx|
|
|
300
|
+
if ctx.state[:collecting_issue]
|
|
301
|
+
issue = ctx.message.text
|
|
302
|
+
# Save to database...
|
|
303
|
+
ctx.reply("Ticket created! We'll contact you.")
|
|
304
|
+
ctx.state.delete(:collecting_issue)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
โ ๏ธ Common Mistakes & Fixes
|
|
310
|
+
|
|
311
|
+
Mistake 1: Assuming ctx.message always exists
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# โ WRONG
|
|
315
|
+
puts ctx.message.text # Crashes if not a message update!
|
|
316
|
+
|
|
317
|
+
# โ
RIGHT
|
|
318
|
+
if ctx.message && ctx.message.text
|
|
319
|
+
puts ctx.message.text
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Mistake 2: Forgetting to answer callbacks
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
# โ WRONG (Telegram will show "loading...")
|
|
327
|
+
bot.on(:callback_query) do |ctx|
|
|
328
|
+
ctx.reply("Button clicked!")
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# โ
RIGHT
|
|
332
|
+
bot.on(:callback_query) do |ctx|
|
|
333
|
+
ctx.answer_callback_query # Tell Telegram we handled it
|
|
334
|
+
ctx.reply("Button clicked!")
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Mistake 3: Not checking chat type
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# โ WRONG (might not work in channels)
|
|
342
|
+
ctx.reply("Hello!")
|
|
343
|
+
|
|
344
|
+
# โ
RIGHT
|
|
345
|
+
if ctx.chat.type != "channel"
|
|
346
|
+
ctx.reply("Hello!")
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
๐ฎ Interactive Learning Challenge
|
|
351
|
+
|
|
352
|
+
Build this in 10 minutes:
|
|
353
|
+
|
|
354
|
+
1. /hello - Replies with user's name
|
|
355
|
+
2. /dice - Rolls random number 1-6
|
|
356
|
+
3. /remember - Remembers what you say
|
|
357
|
+
4. /forget - Forgets everything
|
|
358
|
+
5. Buttons - Yes/No keyboard that works
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
# Starter code - you finish it!
|
|
362
|
+
bot.command('hello') do |ctx|
|
|
363
|
+
# Your code here
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
bot.command('dice') do |ctx|
|
|
367
|
+
# Your code here (hint: rand(1..6))
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
bot.command('remember') do |ctx|
|
|
371
|
+
# Store in ctx.session[:memory]
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
๐ Cheat Sheet
|
|
376
|
+
|
|
377
|
+
Want to... Use... Example
|
|
378
|
+
- Send text ctx.reply() ctx.reply("Hi!")
|
|
379
|
+
- Send photo ctx.photo() ctx.photo("cat.jpg")
|
|
380
|
+
- Get user ID ctx.from.id id = ctx.from.id
|
|
381
|
+
- Check chat type ctx.chat.type if ctx.chat.type == "private"
|
|
382
|
+
- Remember data ctx.session[] ctx.session[:count] = 5
|
|
383
|
+
- Temp data ctx.state[] ctx.state[:asking] = true
|
|
384
|
+
- Button clicks ctx.data if ctx.data == "yes"
|
|
385
|
+
- Edit message ctx.edit_message_text() - -ctx.edit_message_text("Updated!")
|
|
386
|
+
|
|
387
|
+
๐ Next Steps Mastery Path
|
|
388
|
+
|
|
389
|
+
Week 1-2: Use everything in this guide
|
|
390
|
+
Week 3-4: Add keyboards and inline buttons
|
|
391
|
+
Week 5-6: Build multi-step scenes
|
|
392
|
+
Week 7-8: Add database persistence
|
|
393
|
+
Week 9-10: Deploy to cloud
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
Remember: ctx is your Swiss Army knife. The more you use it, the more natural it becomes. Start simple, build gradually, and soon you'll be building bots that feel magical! โจ
|
|
398
|
+
|
|
399
|
+
Your mission: Build one thing from this guide TODAY. Just one. Then build another tomorrow. Consistency beats complexity every time.
|
data/lib/api/client.rb
CHANGED
|
@@ -27,35 +27,36 @@ module Telegem
|
|
|
27
27
|
}
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
|
-
|
|
31
30
|
def call(method, params = {})
|
|
32
31
|
url = "#{BASE_URL}/bot#{@token}/#{method}"
|
|
33
32
|
@logger.debug("API Call: #{method}") if @logger
|
|
34
|
-
@http.post(url, json: params.compact)
|
|
33
|
+
@http.post(url, json: params.compact).wait
|
|
35
34
|
end
|
|
35
|
+
def call!(method, params, &callback)
|
|
36
|
+
url = "#{BASE_URL}/bot#{@token}/#{method}"
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
@http.post(url, json: params)
|
|
39
|
+
.on_complete do |response|
|
|
40
|
+
if response.status == 200
|
|
41
|
+
json = response.json
|
|
42
|
+
if json && json['ok']
|
|
43
|
+
callback.call(json['result']) if callback
|
|
44
|
+
@logger.debug("API Response: #{json}") if @logger
|
|
45
|
+
else
|
|
46
|
+
error_msg = json ? json['description'] : "No JSON response"
|
|
47
|
+
error_code = json['error_code'] if json
|
|
48
|
+
raise APIError.new("API Error: #{error_msg}", error_code)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
raise NetworkError.new("HTTP #{response.status}")
|
|
52
|
+
end
|
|
53
|
+
rescue JSON::ParserError
|
|
54
|
+
raise NetworkError.new("Invalid JSON response")
|
|
55
|
+
rescue => e
|
|
56
|
+
raise e
|
|
57
|
+
end
|
|
58
|
+
.on_error { |error| callback.call(nil, error) if callback }
|
|
54
59
|
end
|
|
55
|
-
|
|
56
|
-
nil
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
60
|
|
|
60
61
|
def upload(method, params)
|
|
61
62
|
url = "#{BASE_URL}/bot#{@token}/#{method}"
|