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.
data/docs/sessions.md ADDED
@@ -0,0 +1,544 @@
1
+ # Session Management
2
+
3
+ Sessions persist data between updates, enabling stateful conversations and user preferences. Telegem supports multiple storage backends with automatic loading and saving.
4
+
5
+ ## How Sessions Work
6
+
7
+ - Sessions are user-specific key-value stores
8
+ - Data persists across messages and bot restarts
9
+ - Automatic loading before handlers, saving after
10
+ - TTL (time-to-live) support for automatic cleanup
11
+
12
+ ## Basic Usage
13
+
14
+ ```ruby
15
+ bot.command('start') do |ctx|
16
+ ctx.session[:visit_count] ||= 0
17
+ ctx.session[:visit_count] += 1
18
+
19
+ ctx.reply("Visit ##{ctx.session[:visit_count]}")
20
+ end
21
+
22
+ bot.command('set_name') do |ctx|
23
+ name = ctx.command_args
24
+ ctx.session[:name] = name
25
+ ctx.reply("Name set to #{name}")
26
+ end
27
+
28
+ bot.command('my_name') do |ctx|
29
+ name = ctx.session[:name] || 'unknown'
30
+ ctx.reply("Your name is #{name}")
31
+ end
32
+ ```
33
+
34
+ ## Session Storage Backends
35
+
36
+ ### Memory Store (Development)
37
+
38
+ ```ruby
39
+ # Default store
40
+ store = Telegem::Session::MemoryStore.new
41
+
42
+ bot = Telegem.new('TOKEN', session_store: store)
43
+ ```
44
+
45
+ Features:
46
+ - Fast in-memory storage
47
+ - No persistence across restarts
48
+ - Automatic cleanup with TTL
49
+ - Good for development/testing
50
+
51
+ ### Redis Store (Production)
52
+
53
+ ```ruby
54
+ require 'redis'
55
+
56
+ redis = Redis.new(url: ENV['REDIS_URL'])
57
+ store = Telegem::Session::RedisStore.new(redis)
58
+
59
+ bot = Telegem.new('TOKEN', session_store: store)
60
+ ```
61
+
62
+ Features:
63
+ - Persistent across deployments
64
+ - Scalable and fast
65
+ - Automatic serialization
66
+ - Redis clustering support
67
+
68
+ ### Custom Store
69
+
70
+ ```ruby
71
+ class DatabaseStore
72
+ def initialize(db_connection)
73
+ @db = db_connection
74
+ end
75
+
76
+ def get(user_id)
77
+ data = @db.query("SELECT session_data FROM sessions WHERE user_id = ?", user_id)
78
+ data ? JSON.parse(data) : {}
79
+ end
80
+
81
+ def set(user_id, data)
82
+ json_data = data.to_json
83
+ @db.execute("INSERT OR REPLACE INTO sessions (user_id, session_data) VALUES (?, ?)", user_id, json_data)
84
+ end
85
+ end
86
+
87
+ store = DatabaseStore.new(my_db_connection)
88
+ bot = Telegem.new('TOKEN', session_store: store)
89
+ ```
90
+
91
+ ## Session Configuration
92
+
93
+ ### Memory Store Options
94
+
95
+ ```ruby
96
+ store = Telegem::Session::MemoryStore.new(
97
+ default_ttl: 3600, # 1 hour default TTL
98
+ cleanup_interval: 300, # Cleanup every 5 minutes
99
+ backup_path: './sessions.json', # Backup file path
100
+ backup_interval: 60 # Backup every minute
101
+ )
102
+ ```
103
+
104
+ ### Redis Store Options
105
+
106
+ ```ruby
107
+ store = Telegem::Session::RedisStore.new(
108
+ redis: redis_client,
109
+ key_prefix: 'mybot:', # Key prefix
110
+ ttl: 86400 # 24 hours TTL
111
+ )
112
+ ```
113
+
114
+ ## Session Data Types
115
+
116
+ Sessions can store any JSON-serializable data:
117
+
118
+ ```ruby
119
+ # Simple values
120
+ ctx.session[:name] = "John"
121
+ ctx.session[:age] = 25
122
+ ctx.session[:active] = true
123
+
124
+ # Complex objects
125
+ ctx.session[:preferences] = {
126
+ theme: 'dark',
127
+ language: 'en',
128
+ notifications: true
129
+ }
130
+
131
+ # Arrays
132
+ ctx.session[:favorites] = ['item1', 'item2']
133
+
134
+ # Dates (as timestamps)
135
+ ctx.session[:last_login] = Time.now.to_i
136
+ ```
137
+
138
+ ## Session Operations
139
+
140
+ ### Reading Data
141
+
142
+ ```ruby
143
+ # Safe access with defaults
144
+ name = ctx.session[:name] || 'Anonymous'
145
+ count = ctx.session.fetch(:count, 0)
146
+
147
+ # Check existence
148
+ if ctx.session.key?(:user_data)
149
+ # Data exists
150
+ end
151
+
152
+ # Get all keys
153
+ keys = ctx.session.keys
154
+ ```
155
+
156
+ ### Modifying Data
157
+
158
+ ```ruby
159
+ # Set values
160
+ ctx.session[:key] = value
161
+
162
+ # Update existing data
163
+ ctx.session[:count] += 1
164
+ ctx.session[:list] << new_item
165
+
166
+ # Delete data
167
+ ctx.session.delete(:temp_key)
168
+
169
+ # Clear all data
170
+ ctx.session.clear
171
+ ```
172
+
173
+ ### Atomic Operations
174
+
175
+ ```ruby
176
+ # Increment with default
177
+ ctx.session[:counter] ||= 0
178
+ ctx.session[:counter] += 1
179
+
180
+ # Array operations
181
+ ctx.session[:items] ||= []
182
+ ctx.session[:items] << 'new_item'
183
+
184
+ # Hash operations
185
+ ctx.session[:user] ||= {}
186
+ ctx.session[:user][:last_seen] = Time.now.to_i
187
+ ```
188
+
189
+ ## Session Lifecycle
190
+
191
+ ### Automatic Loading
192
+
193
+ Session data loads automatically before each handler:
194
+
195
+ ```ruby
196
+ bot.use Telegem::Session::Middleware.new(store)
197
+ # Sessions load here
198
+ # Handler executes
199
+ # Sessions save here
200
+ ```
201
+
202
+ ### Manual Control
203
+
204
+ ```ruby
205
+ # Force save
206
+ ctx.session[:data] = 'value'
207
+ # Automatically saved after handler
208
+
209
+ # Access raw session data
210
+ raw_data = ctx.session.to_h
211
+ ```
212
+
213
+ ## TTL and Expiration
214
+
215
+ ### Setting TTL
216
+
217
+ ```ruby
218
+ # Per key TTL (memory store)
219
+ ctx.session.set_with_ttl(:temp_data, 'value', ttl: 300) # 5 minutes
220
+
221
+ # Global TTL
222
+ store = Telegem::Session::MemoryStore.new(default_ttl: 3600)
223
+ ```
224
+
225
+ ### Expiration Handling
226
+
227
+ ```ruby
228
+ # Check if key exists and not expired
229
+ if ctx.session.key?(:temp_data)
230
+ # Use data
231
+ else
232
+ # Data expired, handle gracefully
233
+ end
234
+ ```
235
+
236
+ ## Session Security
237
+
238
+ ### Data Sanitization
239
+
240
+ ```ruby
241
+ # Don't store sensitive data
242
+ # BAD
243
+ ctx.session[:password] = user_input
244
+
245
+ # GOOD - store user ID, look up in secure storage
246
+ ctx.session[:user_id] = verified_user_id
247
+ ```
248
+
249
+ ### Session Hijacking Protection
250
+
251
+ ```ruby
252
+ # Validate user identity
253
+ bot.use do |ctx, next_middleware|
254
+ if ctx.from&.id != ctx.session[:verified_user_id]
255
+ ctx.session.clear # Clear suspicious session
256
+ end
257
+ next_middleware.call(ctx)
258
+ end
259
+ ```
260
+
261
+ ## Session Best Practices
262
+
263
+ ### Use Appropriate Data Types
264
+
265
+ ```ruby
266
+ # Good: store IDs, not objects
267
+ ctx.session[:user_id] = user.id
268
+ ctx.session[:chat_id] = chat.id
269
+
270
+ # Bad: store large objects
271
+ ctx.session[:user_object] = user # Large, changes frequently
272
+ ```
273
+
274
+ ### Implement Cleanup
275
+
276
+ ```ruby
277
+ # Clean up old data
278
+ bot.command('logout') do |ctx|
279
+ ctx.session.clear
280
+ ctx.reply("Logged out")
281
+ end
282
+
283
+ # Periodic cleanup for memory store
284
+ store = Telegem::Session::MemoryStore.new(
285
+ default_ttl: 86400, # 24 hours
286
+ cleanup_interval: 3600 # Cleanup hourly
287
+ )
288
+ ```
289
+
290
+ ### Handle Session Errors
291
+
292
+ ```ruby
293
+ bot.use do |ctx, next_middleware|
294
+ begin
295
+ next_middleware.call(ctx)
296
+ rescue => e
297
+ ctx.logger.error("Session error: #{e.message}")
298
+ # Continue without session
299
+ end
300
+ end
301
+ ```
302
+
303
+ ### Monitor Session Usage
304
+
305
+ ```ruby
306
+ # Log session size
307
+ bot.use do |ctx, next_middleware|
308
+ next_middleware.call(ctx)
309
+
310
+ if ctx.session.size > 100
311
+ ctx.logger.warn("Large session for user #{ctx.from&.id}: #{ctx.session.size} keys")
312
+ end
313
+ end
314
+ ```
315
+
316
+ ## Advanced Session Patterns
317
+
318
+ ### User Preferences
319
+
320
+ ```ruby
321
+ def get_user_preference(ctx, key, default = nil)
322
+ ctx.session[:preferences] ||= {}
323
+ ctx.session[:preferences][key] || default
324
+ end
325
+
326
+ def set_user_preference(ctx, key, value)
327
+ ctx.session[:preferences] ||= {}
328
+ ctx.session[:preferences][key] = value
329
+ end
330
+
331
+ bot.command('set_theme') do |ctx|
332
+ theme = ctx.command_args
333
+ set_user_preference(ctx, :theme, theme)
334
+ ctx.reply("Theme set to #{theme}")
335
+ end
336
+ ```
337
+
338
+ ### Conversation State
339
+
340
+ ```ruby
341
+ bot.command('quiz') do |ctx|
342
+ ctx.session[:quiz] = {
343
+ active: true,
344
+ question: 1,
345
+ score: 0,
346
+ answers: []
347
+ }
348
+ ctx.reply("Quiz started! Question 1...")
349
+ end
350
+
351
+ bot.hears(/.+/) do |ctx|
352
+ quiz = ctx.session[:quiz]
353
+ if quiz&.[](:active)
354
+ # Process quiz answer
355
+ process_quiz_answer(ctx, quiz, ctx.message.text)
356
+ end
357
+ end
358
+ ```
359
+
360
+ ### Rate Limiting
361
+
362
+ ```ruby
363
+ bot.use do |ctx, next_middleware|
364
+ user_id = ctx.from&.id
365
+ return next_middleware.call(ctx) unless user_id
366
+
367
+ key = "rate_limit:#{user_id}"
368
+ ctx.session[key] ||= { count: 0, window_start: Time.now.to_i }
369
+
370
+ window_size = 60 # 1 minute
371
+ max_requests = 10
372
+
373
+ now = Time.now.to_i
374
+ rate_data = ctx.session[key]
375
+
376
+ # Reset window if needed
377
+ if now - rate_data[:window_start] > window_size
378
+ rate_data[:count] = 0
379
+ rate_data[:window_start] = now
380
+ end
381
+
382
+ if rate_data[:count] >= max_requests
383
+ ctx.reply("Rate limit exceeded")
384
+ return
385
+ end
386
+
387
+ rate_data[:count] += 1
388
+ next_middleware.call(ctx)
389
+ end
390
+ ```
391
+
392
+ ### Session Migration
393
+
394
+ ```ruby
395
+ # Migrate old session format
396
+ bot.use do |ctx, next_middleware|
397
+ if ctx.session[:old_format]
398
+ # Migrate data
399
+ ctx.session[:new_format] = transform_old_data(ctx.session[:old_format])
400
+ ctx.session.delete(:old_format)
401
+ end
402
+
403
+ next_middleware.call(ctx)
404
+ end
405
+ ```
406
+
407
+ ## Session Storage Implementations
408
+
409
+ ### File-Based Store
410
+
411
+ ```ruby
412
+ class FileStore
413
+ def initialize(file_path)
414
+ @file_path = file_path
415
+ @data = load_data
416
+ end
417
+
418
+ def get(user_id)
419
+ @data[user_id.to_s] || {}
420
+ end
421
+
422
+ def set(user_id, data)
423
+ @data[user_id.to_s] = data
424
+ save_data
425
+ end
426
+
427
+ private
428
+
429
+ def load_data
430
+ File.exist?(@file_path) ? JSON.parse(File.read(@file_path)) : {}
431
+ end
432
+
433
+ def save_data
434
+ File.write(@file_path, @data.to_json)
435
+ end
436
+ end
437
+ ```
438
+
439
+ ### Database Store
440
+
441
+ ```ruby
442
+ class PostgresStore
443
+ def initialize(connection_string)
444
+ @db = PG.connect(connection_string)
445
+ create_table
446
+ end
447
+
448
+ def get(user_id)
449
+ result = @db.exec_params("SELECT data FROM sessions WHERE user_id = $1", [user_id])
450
+ result.any? ? JSON.parse(result[0]['data']) : {}
451
+ end
452
+
453
+ def set(user_id, data)
454
+ json_data = data.to_json
455
+ @db.exec_params(
456
+ "INSERT INTO sessions (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2",
457
+ [user_id, json_data]
458
+ )
459
+ end
460
+
461
+ private
462
+
463
+ def create_table
464
+ @db.exec(%q{
465
+ CREATE TABLE IF NOT EXISTS sessions (
466
+ user_id BIGINT PRIMARY KEY,
467
+ data JSONB NOT NULL,
468
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
469
+ )
470
+ })
471
+ end
472
+ end
473
+ ```
474
+
475
+ ## Session Monitoring and Debugging
476
+
477
+ ### Session Inspector
478
+
479
+ ```ruby
480
+ bot.command('debug_session') do |ctx|
481
+ session_data = ctx.session.to_h
482
+ ctx.reply("Session keys: #{session_data.keys.join(', ')}")
483
+ ctx.reply("Session size: #{session_data.to_json.size} bytes")
484
+ end
485
+ ```
486
+
487
+ ### Session Analytics
488
+
489
+ ```ruby
490
+ class SessionAnalyticsMiddleware
491
+ def initialize
492
+ @stats = {}
493
+ end
494
+
495
+ def call(ctx, next_middleware)
496
+ user_id = ctx.from&.id
497
+ return next_middleware.call(ctx) unless user_id
498
+
499
+ @stats[user_id] ||= { messages: 0, session_size: 0 }
500
+ @stats[user_id][:messages] += 1
501
+ @stats[user_id][:session_size] = ctx.session.size
502
+
503
+ next_middleware.call(ctx)
504
+ end
505
+
506
+ def report
507
+ total_users = @stats.size
508
+ total_messages = @stats.values.sum { |s| s[:messages] }
509
+ avg_session_size = @stats.values.sum { |s| s[:session_size] } / total_users.to_f
510
+
511
+ puts "Session Analytics:"
512
+ puts "Total users: #{total_users}"
513
+ puts "Total messages: #{total_messages}"
514
+ puts "Avg session size: #{avg_session_size.round(2)}"
515
+ end
516
+ end
517
+ ```
518
+
519
+ ## Performance Considerations
520
+
521
+ ### Memory Usage
522
+
523
+ - Large sessions increase memory usage
524
+ - Use TTL to automatically clean up
525
+ - Monitor session sizes in production
526
+
527
+ ### Database Performance
528
+
529
+ - Index user_id in database stores
530
+ - Use connection pooling for database stores
531
+ - Consider caching frequently accessed data
532
+
533
+ ### Redis Optimization
534
+
535
+ ```ruby
536
+ # Use Redis pipelines for bulk operations
537
+ redis.pipelined do
538
+ redis.set("session:#{user_id}", data.to_json)
539
+ redis.expire("session:#{user_id}", ttl)
540
+ end
541
+ ```
542
+
543
+ Sessions are essential for creating stateful, personalized bot experiences. Choose the right storage backend and use sessions wisely to maintain good performance.</content>
544
+ <parameter name="filePath">/home/slick/telegem/docs/sessions.md