telegem 3.3.0 → 3.4.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/.rubocop.yml +57 -0
- data/CHANGELOG.md +121 -0
- data/Gemfile +1 -1
- data/README.md +147 -0
- data/bin/telegem-ssl +71 -25
- data/contributing.md +375 -0
- data/docs/api.md +663 -0
- data/docs/bot.md +332 -0
- data/docs/changelog.md +182 -0
- data/docs/context.md +554 -0
- data/docs/core_concepts.md +218 -0
- data/docs/deployment.md +702 -0
- data/docs/error_handling.md +435 -0
- data/docs/examples.md +752 -0
- data/docs/getting_started.md +151 -0
- data/docs/handlers.md +580 -0
- data/docs/keyboards.md +446 -0
- data/docs/middleware.md +536 -0
- data/docs/plugins.md +384 -0
- data/docs/scenes.md +517 -0
- data/docs/sessions.md +544 -0
- data/docs/testing.md +612 -0
- data/docs/troubleshooting.md +574 -0
- data/docs/types.md +538 -0
- data/docs/webhooks.md +456 -0
- data/lib/api/client.rb +38 -10
- data/lib/api/types.rb +433 -172
- data/lib/core/composer.rb +3 -3
- data/lib/core/context.rb +17 -11
- data/lib/plugins/cc +3 -0
- data/lib/plugins/file_extract.rb +2 -2
- data/lib/plugins/translate.rb +43 -0
- data/lib/session/memory_store.rb +90 -103
- data/lib/session/redis.rb +91 -0
- data/lib/telegem.rb +4 -4
- data/lib/webhook/server.rb +5 -3
- metadata +51 -35
- data/CHANGELOG +0 -95
- data/Contributing.md +0 -161
- data/Readme.md +0 -302
- data/examples/.gitkeep +0 -0
- data/public/.gitkeep +0 -0
data/docs/scenes.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# Scene System
|
|
2
|
+
|
|
3
|
+
Scenes enable multi-step conversations and complex interaction flows. They manage state across multiple messages, perfect for forms, wizards, and guided interactions.
|
|
4
|
+
|
|
5
|
+
## What are Scenes?
|
|
6
|
+
|
|
7
|
+
Scenes are stateful conversation flows that:
|
|
8
|
+
|
|
9
|
+
- Maintain context across multiple messages
|
|
10
|
+
- Guide users through step-by-step processes
|
|
11
|
+
- Handle timeouts and cancellations
|
|
12
|
+
- Store temporary data during the conversation
|
|
13
|
+
|
|
14
|
+
## Basic Scene Creation
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
bot.scene :registration do
|
|
18
|
+
step :ask_name do |ctx|
|
|
19
|
+
ctx.reply("What's your name?")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
step :save_name do |ctx|
|
|
23
|
+
name = ctx.message.text
|
|
24
|
+
ctx.session[:user_name] = name
|
|
25
|
+
ctx.reply("Hi #{name}! What's your email?")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
step :complete do |ctx|
|
|
29
|
+
email = ctx.message.text
|
|
30
|
+
ctx.session[:user_email] = email
|
|
31
|
+
ctx.reply("Registration complete!")
|
|
32
|
+
ctx.leave_scene
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Entering Scenes
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
bot.command('register') do |ctx|
|
|
41
|
+
ctx.enter_scene(:registration)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# With initial data
|
|
45
|
+
bot.command('edit_profile') do |ctx|
|
|
46
|
+
ctx.enter_scene(:edit_profile, current_name: ctx.session[:name])
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Scene Methods
|
|
51
|
+
|
|
52
|
+
### Context Methods
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
ctx.enter_scene(:scene_name) # Enter a scene
|
|
56
|
+
ctx.leave_scene # Leave current scene
|
|
57
|
+
ctx.leave_scene(reason: :cancel) # Leave with reason
|
|
58
|
+
ctx.in_scene? # Check if in scene
|
|
59
|
+
ctx.current_scene # Get current scene name
|
|
60
|
+
ctx.scene_data # Get scene data hash
|
|
61
|
+
ctx.ask("Question?") # Ask question (helper)
|
|
62
|
+
ctx.next_step # Move to next step
|
|
63
|
+
ctx.next_step(:specific_step) # Jump to specific step
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Scene Definition
|
|
67
|
+
|
|
68
|
+
### Basic Structure
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
bot.scene :my_scene do
|
|
72
|
+
# Enter callback (optional)
|
|
73
|
+
on_enter do |ctx|
|
|
74
|
+
ctx.reply("Welcome to the scene!")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Leave callback (optional)
|
|
78
|
+
on_leave do |ctx, reason, data|
|
|
79
|
+
ctx.reply("Scene ended: #{reason}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Steps
|
|
83
|
+
step :step1 do |ctx|
|
|
84
|
+
# Step logic
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
step :step2 do |ctx|
|
|
88
|
+
# More logic
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Step Flow
|
|
94
|
+
|
|
95
|
+
Steps execute in order by default:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
bot.scene :survey do
|
|
99
|
+
step :question1 do |ctx|
|
|
100
|
+
ctx.ask("What's your favorite color?")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
step :question2 do |ctx|
|
|
104
|
+
color = ctx.message.text
|
|
105
|
+
ctx.session[:color] = color
|
|
106
|
+
ctx.ask("What's your age?")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
step :finish do |ctx|
|
|
110
|
+
age = ctx.message.text.to_i
|
|
111
|
+
ctx.session[:age] = age
|
|
112
|
+
ctx.reply("Thanks for the survey!")
|
|
113
|
+
ctx.leave_scene
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Conditional Steps
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
step :check_age do |ctx|
|
|
122
|
+
age = ctx.message.text.to_i
|
|
123
|
+
|
|
124
|
+
if age < 18
|
|
125
|
+
ctx.reply("Must be 18+")
|
|
126
|
+
ctx.leave_scene(reason: :underage)
|
|
127
|
+
else
|
|
128
|
+
ctx.session[:age] = age
|
|
129
|
+
ctx.next_step(:collect_email)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Advanced Scene Features
|
|
135
|
+
|
|
136
|
+
### Timeouts
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
bot.scene :timed_scene do
|
|
140
|
+
timeout 300 # 5 minutes
|
|
141
|
+
|
|
142
|
+
step :start do |ctx|
|
|
143
|
+
ctx.reply("You have 5 minutes...")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
on_leave do |ctx, reason, data|
|
|
147
|
+
if reason == :timeout
|
|
148
|
+
ctx.reply("Time's up!")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Scene Data
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
bot.scene :form do
|
|
158
|
+
step :name do |ctx|
|
|
159
|
+
ctx.ask("Name?")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
step :email do |ctx|
|
|
163
|
+
name = ctx.scene_data[:name] # Access previous step data
|
|
164
|
+
ctx.ask("Email?")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Dynamic Scenes
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# Create scenes programmatically
|
|
173
|
+
def create_quiz_scene(questions)
|
|
174
|
+
bot.scene :quiz do
|
|
175
|
+
questions.each_with_index do |question, index|
|
|
176
|
+
step "q#{index}".to_sym do |ctx|
|
|
177
|
+
if index < questions.size - 1
|
|
178
|
+
ctx.ask(question)
|
|
179
|
+
else
|
|
180
|
+
ctx.reply("Quiz complete!")
|
|
181
|
+
ctx.leave_scene
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
create_quiz_scene(["Q1?", "Q2?", "Q3?"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Scene Lifecycle
|
|
192
|
+
|
|
193
|
+
### Entering a Scene
|
|
194
|
+
|
|
195
|
+
1. `on_enter` callbacks execute
|
|
196
|
+
2. Scene data initializes
|
|
197
|
+
3. First step executes
|
|
198
|
+
|
|
199
|
+
### During Scene
|
|
200
|
+
|
|
201
|
+
- Each message goes to current step handler
|
|
202
|
+
- `ask()` helper sets waiting state
|
|
203
|
+
- Steps can jump to other steps
|
|
204
|
+
- Data persists in `ctx.scene_data`
|
|
205
|
+
|
|
206
|
+
### Leaving a Scene
|
|
207
|
+
|
|
208
|
+
1. `on_leave` callbacks execute
|
|
209
|
+
2. Scene data cleans up
|
|
210
|
+
3. Normal message processing resumes
|
|
211
|
+
|
|
212
|
+
## Scene State Management
|
|
213
|
+
|
|
214
|
+
### Scene Data Storage
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# Scene data is stored in session
|
|
218
|
+
ctx.session[:telegem_scene] = {
|
|
219
|
+
id: "registration",
|
|
220
|
+
step: "ask_name",
|
|
221
|
+
data: { name: "John" },
|
|
222
|
+
entered_at: 1234567890,
|
|
223
|
+
timeout: 300,
|
|
224
|
+
waiting_for_response: true,
|
|
225
|
+
last_question: "What's your name?"
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Accessing Scene Data
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
# In scene steps
|
|
233
|
+
step :process do |ctx|
|
|
234
|
+
data = ctx.scene_data
|
|
235
|
+
name = data[:name]
|
|
236
|
+
email = data[:email]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Outside scenes
|
|
240
|
+
if ctx.in_scene?
|
|
241
|
+
scene_data = ctx.session[:telegem_scene][:data]
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Error Handling
|
|
246
|
+
|
|
247
|
+
### Scene Errors
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
bot.scene :error_prone do
|
|
251
|
+
step :risky do |ctx|
|
|
252
|
+
begin
|
|
253
|
+
risky_operation()
|
|
254
|
+
ctx.next_step
|
|
255
|
+
rescue => e
|
|
256
|
+
ctx.reply("Error occurred")
|
|
257
|
+
ctx.leave_scene(reason: :error)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Timeout Handling
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
bot.scene :with_timeout do
|
|
267
|
+
timeout 60 # 1 minute
|
|
268
|
+
|
|
269
|
+
on_leave do |ctx, reason, data|
|
|
270
|
+
case reason
|
|
271
|
+
when :timeout
|
|
272
|
+
ctx.reply("Scene timed out")
|
|
273
|
+
when :error
|
|
274
|
+
ctx.reply("Scene ended due to error")
|
|
275
|
+
when :manual
|
|
276
|
+
ctx.reply("Scene completed")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Scene Best Practices
|
|
283
|
+
|
|
284
|
+
### Keep Scenes Focused
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# Bad: too many steps
|
|
288
|
+
bot.scene :everything do
|
|
289
|
+
step :login
|
|
290
|
+
step :select_option
|
|
291
|
+
step :process_payment
|
|
292
|
+
step :send_confirmation
|
|
293
|
+
# 10 more steps...
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Good: separate scenes
|
|
297
|
+
bot.scene :auth do
|
|
298
|
+
step :login
|
|
299
|
+
step :verify
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
bot.scene :payment do
|
|
303
|
+
step :select_amount
|
|
304
|
+
step :process
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Validate Input
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
step :collect_email do |ctx|
|
|
312
|
+
email = ctx.message.text
|
|
313
|
+
|
|
314
|
+
unless valid_email?(email)
|
|
315
|
+
ctx.reply("Invalid email. Try again.")
|
|
316
|
+
return # Stay on same step
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
ctx.scene_data[:email] = email
|
|
320
|
+
ctx.next_step
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Provide Escape Options
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
bot.hears(/^cancel$/i) do |ctx|
|
|
328
|
+
if ctx.in_scene?
|
|
329
|
+
ctx.leave_scene(reason: :cancel)
|
|
330
|
+
ctx.reply("Cancelled.")
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
bot.hears(/^back$/i) do |ctx|
|
|
335
|
+
if ctx.in_scene?
|
|
336
|
+
# Implement back logic
|
|
337
|
+
ctx.reply("Going back...")
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Use Helpers
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
def ask_with_validation(ctx, question, validator)
|
|
346
|
+
ctx.ask(question)
|
|
347
|
+
|
|
348
|
+
# In next step
|
|
349
|
+
response = ctx.message.text
|
|
350
|
+
if validator.call(response)
|
|
351
|
+
# Valid
|
|
352
|
+
else
|
|
353
|
+
ctx.reply("Invalid input")
|
|
354
|
+
# Retry
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Complex Scene Examples
|
|
360
|
+
|
|
361
|
+
### Multi-choice Survey
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
bot.scene :survey do
|
|
365
|
+
step :start do |ctx|
|
|
366
|
+
keyboard = Telegem.keyboard do
|
|
367
|
+
row "Yes", "No", "Maybe"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
ctx.reply("Do you like pizza?", reply_markup: keyboard)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
step :follow_up do |ctx|
|
|
374
|
+
answer = ctx.message.text
|
|
375
|
+
ctx.scene_data[:pizza] = answer
|
|
376
|
+
|
|
377
|
+
if answer == "Yes"
|
|
378
|
+
ctx.ask("What's your favorite topping?")
|
|
379
|
+
else
|
|
380
|
+
ctx.reply("Survey complete!")
|
|
381
|
+
ctx.leave_scene
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
step :complete do |ctx|
|
|
386
|
+
topping = ctx.message.text
|
|
387
|
+
ctx.scene_data[:topping] = topping
|
|
388
|
+
ctx.reply("Thanks for your feedback!")
|
|
389
|
+
ctx.leave_scene
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### File Upload Scene
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
bot.scene :upload do
|
|
398
|
+
step :request_file do |ctx|
|
|
399
|
+
ctx.reply("Please send me a document")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
step :process_file do |ctx|
|
|
403
|
+
unless ctx.message.document
|
|
404
|
+
ctx.reply("Please send a document")
|
|
405
|
+
return
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Process file
|
|
409
|
+
file_id = ctx.message.document.file_id
|
|
410
|
+
ctx.download_file(file_id, "uploads/#{file_id}")
|
|
411
|
+
|
|
412
|
+
ctx.reply("File uploaded successfully!")
|
|
413
|
+
ctx.leave_scene
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Payment Flow
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
bot.scene :payment do
|
|
422
|
+
step :select_amount do |ctx|
|
|
423
|
+
keyboard = Telegem.keyboard do
|
|
424
|
+
row "$10", "$25", "$50"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
ctx.reply("Select amount:", reply_markup: keyboard)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
step :confirm do |ctx|
|
|
431
|
+
amount = ctx.message.text.delete('$').to_i
|
|
432
|
+
ctx.scene_data[:amount] = amount
|
|
433
|
+
|
|
434
|
+
keyboard = Telegem.inline do
|
|
435
|
+
callback "Confirm", "confirm_payment"
|
|
436
|
+
callback "Cancel", "cancel_payment"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
ctx.reply("Pay $#{amount}?", reply_markup: keyboard)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
step :process do |ctx|
|
|
443
|
+
# Process payment
|
|
444
|
+
ctx.reply("Payment successful!")
|
|
445
|
+
ctx.leave_scene
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
bot.callback_query(/^confirm_payment/) do |ctx|
|
|
450
|
+
ctx.answer_callback_query("Processing payment...")
|
|
451
|
+
ctx.next_step(:process)
|
|
452
|
+
end
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Scene Integration with Middleware
|
|
456
|
+
|
|
457
|
+
Scenes work with the scene middleware (included by default):
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
# Scene middleware intercepts updates when user is in scene
|
|
461
|
+
# Routes to appropriate scene step
|
|
462
|
+
# Handles timeouts and cleanup
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Testing Scenes
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
# Test scene flow
|
|
469
|
+
def test_scene_flow
|
|
470
|
+
# Enter scene
|
|
471
|
+
simulate_message(bot, '/start_scene')
|
|
472
|
+
|
|
473
|
+
# Simulate user responses
|
|
474
|
+
simulate_message(bot, 'John')
|
|
475
|
+
simulate_message(bot, 'john@example.com')
|
|
476
|
+
|
|
477
|
+
# Check final state
|
|
478
|
+
assert user_registered?('john@example.com')
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Test timeout
|
|
482
|
+
def test_scene_timeout
|
|
483
|
+
enter_scene(:timed_scene)
|
|
484
|
+
|
|
485
|
+
# Fast forward time
|
|
486
|
+
Timecop.travel(6.minutes)
|
|
487
|
+
|
|
488
|
+
simulate_message(bot, 'response')
|
|
489
|
+
|
|
490
|
+
# Should have timed out
|
|
491
|
+
assert !in_scene?
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Scene Limitations
|
|
496
|
+
|
|
497
|
+
- Scenes are user-specific (one scene per user)
|
|
498
|
+
- Scene data stored in session (memory/Redis limits apply)
|
|
499
|
+
- No concurrent scenes per user
|
|
500
|
+
- Scenes block normal message processing
|
|
501
|
+
|
|
502
|
+
## Alternative Approaches
|
|
503
|
+
|
|
504
|
+
For simple interactions, consider:
|
|
505
|
+
|
|
506
|
+
- Inline keyboards with callback queries
|
|
507
|
+
- State machines in session
|
|
508
|
+
- Multiple command handlers
|
|
509
|
+
|
|
510
|
+
Use scenes when you need:
|
|
511
|
+
- Guided step-by-step flows
|
|
512
|
+
- Temporary data collection
|
|
513
|
+
- Complex validation logic
|
|
514
|
+
- Timeout handling
|
|
515
|
+
|
|
516
|
+
Scenes are powerful for creating interactive, stateful conversations in your Telegram bot.</content>
|
|
517
|
+
<parameter name="filePath">/home/slick/telegem/docs/scenes.md
|