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.
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
- def call!(method, params = {})
38
- @mutex.synchroniz do
39
- url = "#{BASE_URL}/bot#{@token}/#{method}"
40
-
41
- @logger.debug("API Call (sync): #{method}") if @logger
42
-
43
- begin
44
- response = @http.post(url, json: params.compact)
45
-
46
- if response.respond_to?(:status) && response.status == 200
47
- json = response.json
48
- return json['result'] if json && json['ok']
49
- end
50
-
51
- @logger.error("API Error: HTTP #{response.status}") if response.respond_to?(:status) && @logger
52
- rescue => e
53
- @logger.error("Exception: #{e.message}") if @logger
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}"