telegem 3.3.0 → 3.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 156ed056e2f5201d26aaa636b1bd2fdace0499dedca7aa52cff1692546480733
4
- data.tar.gz: b0fef7cc585db195c4eeef5c0d656a659b1d12f8e9ea25506745c9318bc5dca8
3
+ metadata.gz: 0d2afc9e262ffced76a28974a69c6e652bf433b2b6da80c6827d45e6da43002a
4
+ data.tar.gz: 61f440f2523473e3f5435075bfece0519c374f4aa498d789cc6752c5ce285abf
5
5
  SHA512:
6
- metadata.gz: a99c675b08fec66843ecc4d41880a74043a0498a486db404049e63eeb523ab746f186c482ce828550e010fa8f4f4ebd1102507e73532fb681c912c48c144417e
7
- data.tar.gz: 0dc7360428b2d1fcfe383941f54c0b44f75af45c35eb7cb9cfbbfee190445c095bd5fac6007cd2ec885736ec8eea38ca9e286030b9b4486cd97ac4bef353d96c
6
+ metadata.gz: 378d55910d0b38b255b35ea357af4cb88f45000362b9d16688dff82a02fe3e5119e71e591c2cec61556d9c9cc82c0edd914ba7cd153ba1cae04c0e97b3f6d229
7
+ data.tar.gz: 527cb2833282c03d7f19145d7421e6df5d867ffbb479846ac9e969a8be339450c7358fdefaced5952dfc7d96b999348253a2494b2a2fe3d9c55480073587a421
data/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ # Telegem Changelog
2
+
3
+ ## v3.3.1(lastest)
4
+ ### Features
5
+ - improved memeorystore to include diskbackup
6
+ - added 'telegem-init' cli
7
+
8
+
9
+ ## v3.1.1
10
+
11
+ ### New Features
12
+
13
+ -FileExtractor Plugin: New plugin for extracting content from various file types (PDF, JSON, HTML, TXT)
14
+ - Async File Download: Added download method to API client for downloading Telegram files
15
+ - Context File Helpers: Added download_file, download_photo, download_document methods to Context
16
+ - Extended File Support: Plugin supports PDF text extraction, JSON parsing, HTML/raw text processing
17
+ - Async/Sync Dual Mode: All file operations available in both sync (download) and async (download!) modes
18
+
19
+ ## v3.1.0
20
+
21
+ ### features
22
+ - BREAKING: Rewrote polling system to prevent duplicate messages
23
+ - Fixed thread deadlock in async polling loop
24
+ - Added scene_middleware.rb for scene-based conversations
25
+ - Improved MemoryStore with TTL and thread safety
26
+ - Enhanced keyboard markup builder with web_app support
27
+ - Added message reaction and chat boost update types
28
+ - Fixed callback query handling for inline keyboards
29
+
30
+ ## v3.0.0
31
+ ### features
32
+
33
+ - BREAKING: Complete async rewrite with async gem
34
+ - New HTTP client using HTTPX with proper async/await pattern
35
+ - Added scene system for multi-step conversations
36
+ - Middleware composer system for plugin architecture
37
+ - Type system with dynamic accessors for Telegram objects
38
+ - Session management with memory store
39
+ - Rate limiting middleware
40
+ - File upload support via multipart forms
41
+
42
+ ## v2.0.0
43
+
44
+ - BREAKING: Ruby 3.0+ requirement
45
+ - Added webhook support with Rack middleware
46
+ - Inline query and callback query handlers
47
+ - Location, contact, and poll answer handlers
48
+ - Keyboard markup helpers (Telegem::Markup)
49
+ - Improved error handling with custom error classes
50
+ - Logging integration with configurable loggers
51
+
52
+ ## v1.5.0
53
+
54
+ - Added command argument parsing (ctx.command_args)
55
+ - Message entity parsing (mentions, hashtags, bot commands)
56
+ - Chat member update handlers
57
+ - Pre-checkout and shipping query support
58
+ - File download helper methods
59
+ - Context helper methods for common API calls
60
+
61
+ ## v1.0.0
62
+
63
+ - Stable API release
64
+ - Message handlers with text pattern matching
65
+ - Command handlers with regex support
66
+ - Basic context object with chat/message accessors
67
+ - Simple API client with error handling
68
+ - Polling and webhook modes
69
+ - Configuration options for timeout and limits
70
+
71
+ ## v0.5.0
72
+
73
+ - Added callback query support
74
+ - Inline keyboard builder
75
+ - Message editing and deletion helpers
76
+ - Media sending methods (photo, document, audio, video)
77
+ - Chat action methods (typing, upload indicators)
78
+
79
+ ## v0.3.0
80
+
81
+ - Middleware system with bot.use
82
+ - Session management foundation
83
+ - Basic rate limiting
84
+ - Command filtering by chat type
85
+ - Improved logging with debug levels
86
+
87
+ ## v0.2.0
88
+
89
+ - Basic polling implementation
90
+ - Message type detection (text, photo, document)
91
+ - Command parsing with arguments
92
+ - Simple reply methods
93
+ - Error handling for API calls
94
+
95
+ ## v0.1.0 (Initial Release)
96
+
97
+ - Basic Telegram Bot API wrapper
98
+ - Send/receive messages
99
+ - Simple command handling
100
+ - Minimal dependencies (just httparty)
101
+ - Support for basic message types
102
+
103
+ ---
data/Gemfile CHANGED
@@ -5,5 +5,5 @@ gemspec
5
5
  group :development do
6
6
  gem 'rake', '~> 13.0'
7
7
  gem 'rspec', '~> 3.0'
8
- gem 'pry', '~> 0.14.0'
8
+ gem 'pry', '~> 0.16.0'
9
9
  end
data/Readme.md CHANGED
@@ -4,10 +4,6 @@ Modern, blazing-fast async Telegram Bot API for Ruby - Inspired by Telegraf, bui
4
4
 
5
5
  ![Gem Version](https://badge.fury.io/rb/telegem.svg) ![GitLab](https://img.shields.io/badge/gitlab-telegem-orange) ![Ruby Version](https://img.shields.io/badge/Ruby-3.0+-red.svg) ![License](https://img.shields.io/badge/License-MIT-blue.svg) ![Async I/O](https://img.shields.io/badge/Async-I/O-green.svg)
6
6
 
7
- ![GitLab stars](https://img.shields.io/gitlab/stars/ruby-telegem/telegem?style=for-the-badge&logo=gitlab&color=orange)
8
- ![GitLab contributors](https://img.shields.io/gitlab/contributors/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
9
- ![GitLab last commit](https://img.shields.io/gitlab/last-commit/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
10
- ![GitLab license](https://img.shields.io/gitlab/license/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
11
7
 
12
8
 
13
9
 
@@ -299,4 +295,4 @@ ruby -r telegem -e "puts 'Welcome to Telegem! 🚀'"
299
295
 
300
296
  ---
301
297
 
302
- Built with ❤️ for the Ruby community. Happy bot building! 🤖✨
298
+ Built with ❤️ for the Ruby community. Happy bot building! 🤖✨
data/bin/telegem-init ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # bin/telegem-init
3
+
4
+ require 'fileutils'
5
+
6
+ puts " Creating a telegem application..."
7
+
8
+ # Create the nested directory structure
9
+ FileUtils.mkdir_p("src/handlers")
10
+
11
+
12
+ code = <<~RUBY
13
+ require 'telegem'
14
+ require 'dotenv/load'
15
+
16
+ # Load all handlers from the handlers directory relative to this file
17
+ Dir[File.join(__dir__, 'handlers', '*.rb')].each { |file| require file }
18
+
19
+ bot = Telegem.new(ENV['BOT_TOKEN'])
20
+
21
+ puts "Bot is starting..."
22
+ bot.start_polling
23
+ RUBY
24
+
25
+ File.write('src/bot.rb', code)
26
+
27
+ # Optional: Create a blank .env file so the bot doesn't crash on load
28
+ File.write('.env', "BOT_TOKEN=your_token_here") unless File.exist?('.env')
29
+
30
+ puts "Successfully created src/bot.rb"
31
+ puts " Structure created: src/handlers/"
32
+ puts " Done working. Run your bot with: ruby src/bot.rb"
data/lib/api/types.rb CHANGED
@@ -62,6 +62,21 @@ module Telegem
62
62
  @_accessors_defined[name] = true
63
63
  end
64
64
 
65
+ # helpers for converting nested objects
66
+ def wrap(key, klass)
67
+ if @_raw_data[key] && !@_raw_data[key].is_a?(klass)
68
+ @_raw_data[key] = klass.new(@_raw_data[key])
69
+ end
70
+ end
71
+
72
+ def wrap_array(key, klass)
73
+ if @_raw_data[key] && @_raw_data[key].is_a?(Array)
74
+ @_raw_data[key] = @_raw_data[key].map do |v|
75
+ v.is_a?(klass) ? v : klass.new(v)
76
+ end
77
+ end
78
+ end
79
+
65
80
  def snake_to_camel(str)
66
81
  str.gsub(/_([a-z])/) { $1.upcase }
67
82
  end
@@ -250,84 +265,104 @@ module Telegem
250
265
  private
251
266
 
252
267
  def convert_complex_fields
253
- if @_raw_data['date'] && !@_raw_data['date'].is_a?(Time)
254
- @_raw_data['date'] = Time.at(@_raw_data['date'])
255
- end
256
-
257
- if @_raw_data['edit_date'] && !@_raw_data['edit_date'].is_a?(Time)
258
- @_raw_data['edit_date'] = Time.at(@_raw_data['edit_date'])
259
- end
260
-
261
- if @_raw_data['forward_date'] && !@_raw_data['forward_date'].is_a?(Time)
262
- @_raw_data['forward_date'] = Time.at(@_raw_data['forward_date'])
263
- end
264
-
265
- @_raw_data['from'] = User.new(@_raw_data['from']) if @_raw_data['from'] && !@_raw_data['from'].is_a?(User)
266
- @_raw_data['chat'] = Chat.new(@_raw_data['chat']) if @_raw_data['chat'] && !@_raw_data['chat'].is_a?(Chat)
267
- @_raw_data['via_bot'] = User.new(@_raw_data['via_bot']) if @_raw_data['via_bot'] && !@_raw_data['via_bot'].is_a?(User)
268
- @_raw_data['forward_from'] = User.new(@_raw_data['forward_from']) if @_raw_data['forward_from'] && !@_raw_data['forward_from'].is_a?(User)
269
- @_raw_data['forward_from_chat'] = Chat.new(@_raw_data['forward_from_chat']) if @_raw_data['forward_from_chat'] && !@_raw_data['forward_from_chat'].is_a?(Chat)
270
- @_raw_data['left_chat_member'] = User.new(@_raw_data['left_chat_member']) if @_raw_data['left_chat_member'] && !@_raw_data['left_chat_member'].is_a?(User)
271
-
272
- if @_raw_data['entities'] && @_raw_data['entities'].is_a?(Array)
273
- @_raw_data['entities'] = @_raw_data['entities'].map do |e|
274
- e.is_a?(MessageEntity) ? e : MessageEntity.new(e)
275
- end
276
- end
277
-
278
- if @_raw_data['caption_entities'] && @_raw_data['caption_entities'].is_a?(Array)
279
- @_raw_data['caption_entities'] = @_raw_data['caption_entities'].map do |e|
280
- e.is_a?(MessageEntity) ? e : MessageEntity.new(e)
281
- end
282
- end
283
-
284
- if @_raw_data['reply_to_message'] && !@_raw_data['reply_to_message'].is_a?(Message)
285
- @_raw_data['reply_to_message'] = Message.new(@_raw_data['reply_to_message'])
286
- end
287
-
288
- if @_raw_data['pinned_message'] && !@_raw_data['pinned_message'].is_a?(Message)
289
- @_raw_data['pinned_message'] = Message.new(@_raw_data['pinned_message'])
290
- end
291
-
292
- if @_raw_data['new_chat_members'] && @_raw_data['new_chat_members'].is_a?(Array)
293
- @_raw_data['new_chat_members'] = @_raw_data['new_chat_members'].map do |u|
294
- u.is_a?(User) ? u : User.new(u)
295
- end
296
- end
297
-
268
+ # time conversions
269
+ @_raw_data['date'] = Time.at(@_raw_data['date']) if @_raw_data['date'] && !@_raw_data['date'].is_a?(Time)
270
+ @_raw_data['edit_date'] = Time.at(@_raw_data['edit_date']) if @_raw_data['edit_date'] && !@_raw_data['edit_date'].is_a?(Time)
271
+ @_raw_data['forward_date'] = Time.at(@_raw_data['forward_date']) if @_raw_data['forward_date'] && !@_raw_data['forward_date'].is_a?(Time)
272
+
273
+ # basic object wrappers
274
+ wrap('from', User)
275
+ wrap('chat', Chat)
276
+ wrap('via_bot', User)
277
+ wrap('forward_from', User)
278
+ wrap('forward_from_chat', Chat)
279
+ wrap('left_chat_member', User)
280
+
281
+ wrap_array('entities', MessageEntity)
282
+ wrap_array('caption_entities', MessageEntity)
283
+
284
+ wrap('reply_to_message', Message)
285
+ wrap('pinned_message', Message)
286
+ wrap_array('new_chat_members', User)
287
+
288
+ # media and other nested types
289
+ wrap('contact', Contact)
290
+ wrap('location', Location)
291
+ wrap('venue', Venue)
292
+ wrap('dice', Dice)
293
+ wrap('poll', Poll)
294
+ wrap('proximity_alert_triggered', ProximityAlertTriggered)
295
+ wrap('web_app_data', WebAppData)
296
+
297
+ wrap('animation', Animation)
298
+ wrap('audio', Audio)
299
+ wrap('document', Document)
300
+ wrap('video', Video)
301
+ wrap('voice', Voice)
302
+ wrap('video_note', VideoNote)
303
+ wrap('sticker', Sticker)
304
+
305
+ wrap('invoice', Invoice)
306
+ wrap('successful_payment', SuccessfulPayment)
307
+ wrap('reply_markup', BaseType)
308
+
309
+ wrap('passport_data', PassportData)
310
+
311
+ wrap('video_chat_scheduled', VideoChatScheduled)
312
+ wrap('video_chat_started', VideoChatStarted)
313
+ wrap('video_chat_ended', VideoChatEnded)
314
+ wrap('video_chat_participants_invited', VideoChatParticipantsInvited)
315
+ wrap('video_chat_location', VideoChatLocation)
316
+
317
+ # new message event objects introduced in later API versions
318
+ wrap('message_auto_delete_timer_changed', MessageAutoDeleteTimerChanged)
319
+ wrap('forum_topic_created', ForumTopicCreated)
320
+ wrap('forum_topic_edited', ForumTopicEdited)
321
+ wrap('forum_topic_closed', ForumTopicClosed)
322
+ wrap('forum_topic_reopened', ForumTopicReopened)
323
+ wrap('general_forum_topic_hidden', GeneralForumTopicHidden)
324
+ wrap('general_forum_topic_unhidden', GeneralForumTopicUnhidden)
325
+ wrap('write_access_allowed', WriteAccessAllowed)
326
+
327
+ # arrays of sizes and photos
328
+ wrap_array('photo', PhotoSize)
329
+ wrap_array('new_chat_photo', PhotoSize)
330
+
331
+ # fall back to original media wrapper for backward compatibility
298
332
  wrap_media_objects
299
333
  end
300
334
 
301
335
  def wrap_media_objects
302
- # Media files
303
- @_raw_data['document'] = BaseType.new(@_raw_data['document']) if @_raw_data['document'] && !@_raw_data['document'].is_a?(BaseType)
304
- @_raw_data['audio'] = BaseType.new(@_raw_data['audio']) if @_raw_data['audio'] && !@_raw_data['audio'].is_a?(BaseType)
305
- @_raw_data['video'] = BaseType.new(@_raw_data['video']) if @_raw_data['video'] && !@_raw_data['video'].is_a?(BaseType)
306
- @_raw_data['voice'] = BaseType.new(@_raw_data['voice']) if @_raw_data['voice'] && !@_raw_data['voice'].is_a?(BaseType)
307
- @_raw_data['video_note'] = BaseType.new(@_raw_data['video_note']) if @_raw_data['video_note'] && !@_raw_data['video_note'].is_a?(BaseType)
308
- @_raw_data['sticker'] = BaseType.new(@_raw_data['sticker']) if @_raw_data['sticker'] && !@_raw_data['sticker'].is_a?(BaseType)
309
-
336
+ # Media files (fall‑back to generic types if no specific class defined)
337
+ @_raw_data['document'] = Document.new(@_raw_data['document']) if @_raw_data['document'] && !@_raw_data['document'].is_a?(Document)
338
+ @_raw_data['animation'] = Animation.new(@_raw_data['animation']) if @_raw_data['animation'] && !@_raw_data['animation'].is_a?(Animation)
339
+ @_raw_data['audio'] = Audio.new(@_raw_data['audio']) if @_raw_data['audio'] && !@_raw_data['audio'].is_a?(Audio)
340
+ @_raw_data['video'] = Video.new(@_raw_data['video']) if @_raw_data['video'] && !@_raw_data['video'].is_a?(Video)
341
+ @_raw_data['voice'] = Voice.new(@_raw_data['voice']) if @_raw_data['voice'] && !@_raw_data['voice'].is_a?(Voice)
342
+ @_raw_data['video_note'] = VideoNote.new(@_raw_data['video_note']) if @_raw_data['video_note'] && !@_raw_data['video_note'].is_a?(VideoNote)
343
+ @_raw_data['sticker'] = Sticker.new(@_raw_data['sticker']) if @_raw_data['sticker'] && !@_raw_data['sticker'].is_a?(Sticker)
344
+
310
345
  # Photo array
311
346
  if @_raw_data['photo'] && @_raw_data['photo'].is_a?(Array)
312
347
  @_raw_data['photo'] = @_raw_data['photo'].map do |p|
313
- p.is_a?(BaseType) ? p : BaseType.new(p)
348
+ p.is_a?(PhotoSize) ? p : PhotoSize.new(p)
314
349
  end
315
350
  end
316
-
351
+
317
352
  # Contact, location, venue
318
- @_raw_data['contact'] = BaseType.new(@_raw_data['contact']) if @_raw_data['contact'] && !@_raw_data['contact'].is_a?(BaseType)
319
- @_raw_data['location'] = BaseType.new(@_raw_data['location']) if @_raw_data['location'] && !@_raw_data['location'].is_a?(BaseType)
320
- @_raw_data['venue'] = BaseType.new(@_raw_data['venue']) if @_raw_data['venue'] && !@_raw_data['venue'].is_a?(BaseType)
321
-
353
+ @_raw_data['contact'] = Contact.new(@_raw_data['contact']) if @_raw_data['contact'] && !@_raw_data['contact'].is_a?(Contact)
354
+ @_raw_data['location'] = Location.new(@_raw_data['location']) if @_raw_data['location'] && !@_raw_data['location'].is_a?(Location)
355
+ @_raw_data['venue'] = Venue.new(@_raw_data['venue']) if @_raw_data['venue'] && !@_raw_data['venue'].is_a?(Venue)
356
+
322
357
  # Payment & other
323
- @_raw_data['invoice'] = BaseType.new(@_raw_data['invoice']) if @_raw_data['invoice'] && !@_raw_data['invoice'].is_a?(BaseType)
324
- @_raw_data['successful_payment'] = BaseType.new(@_raw_data['successful_payment']) if @_raw_data['successful_payment'] && !@_raw_data['successful_payment'].is_a?(BaseType)
358
+ @_raw_data['invoice'] = Invoice.new(@_raw_data['invoice']) if @_raw_data['invoice'] && !@_raw_data['invoice'].is_a?(Invoice)
359
+ @_raw_data['successful_payment'] = SuccessfulPayment.new(@_raw_data['successful_payment']) if @_raw_data['successful_payment'] && !@_raw_data['successful_payment'].is_a?(SuccessfulPayment)
325
360
  @_raw_data['reply_markup'] = BaseType.new(@_raw_data['reply_markup']) if @_raw_data['reply_markup'] && !@_raw_data['reply_markup'].is_a?(BaseType)
326
-
361
+
327
362
  # Chat photo array
328
363
  if @_raw_data['new_chat_photo'] && @_raw_data['new_chat_photo'].is_a?(Array)
329
364
  @_raw_data['new_chat_photo'] = @_raw_data['new_chat_photo'].map do |p|
330
- p.is_a?(BaseType) ? p : BaseType.new(p)
365
+ p.is_a?(PhotoSize) ? p : PhotoSize.new(p)
331
366
  end
332
367
  end
333
368
  end
@@ -428,15 +463,218 @@ module Telegem
428
463
  private
429
464
 
430
465
  def convert_update_objects
431
- @_raw_data['message'] = Message.new(@_raw_data['message']) if @_raw_data['message'] && !@_raw_data['message'].is_a?(Message)
432
- @_raw_data['edited_message'] = Message.new(@_raw_data['edited_message']) if @_raw_data['edited_message'] && !@_raw_data['edited_message'].is_a?(Message)
433
- @_raw_data['channel_post'] = Message.new(@_raw_data['channel_post']) if @_raw_data['channel_post'] && !@_raw_data['channel_post'].is_a?(Message)
434
- @_raw_data['edited_channel_post'] = Message.new(@_raw_data['edited_channel_post']) if @_raw_data['edited_channel_post'] && !@_raw_data['edited_channel_post'].is_a?(Message)
435
-
436
- if @_raw_data['callback_query'] && !@_raw_data['callback_query'].is_a?(CallbackQuery)
437
- @_raw_data['callback_query'] = CallbackQuery.new(@_raw_data['callback_query'])
466
+ wrap('message', Message)
467
+ wrap('edited_message', Message)
468
+ wrap('channel_post', Message)
469
+ wrap('edited_channel_post', Message)
470
+
471
+ wrap('inline_query', InlineQuery)
472
+ wrap('chosen_inline_result', ChosenInlineResult)
473
+ wrap('callback_query', CallbackQuery)
474
+ wrap('shipping_query', ShippingQuery)
475
+ wrap('pre_checkout_query', PreCheckoutQuery)
476
+ wrap('poll', Poll)
477
+ wrap('poll_answer', PollAnswer)
478
+ wrap('my_chat_member', ChatMemberUpdated)
479
+ wrap('chat_member', ChatMemberUpdated)
480
+ wrap('chat_join_request', ChatJoinRequest)
481
+ wrap('forum_topic_created', ForumTopicCreated)
482
+ wrap('forum_topic_edited', ForumTopicEdited)
483
+ wrap('forum_topic_closed', ForumTopicClosed)
484
+ wrap('forum_topic_reopened', ForumTopicReopened)
485
+ wrap('general_forum_topic_hidden', GeneralForumTopicHidden)
486
+ wrap('general_forum_topic_unhidden', GeneralForumTopicUnhidden)
487
+ wrap('write_access_allowed', WriteAccessAllowed)
488
+ end
489
+ end
490
+
491
+ # additional types returned by various methods / updates
492
+ class PhotoSize < BaseType; end
493
+ class Audio < BaseType; end
494
+ class Document < BaseType; end
495
+ class Video < BaseType; end
496
+ class Voice < BaseType; end
497
+ class VideoNote < BaseType; end
498
+ class Animation < BaseType; end
499
+ class Sticker < BaseType; end
500
+ class Contact < BaseType; end
501
+ class Dice < BaseType; end
502
+
503
+ class Location < BaseType; end
504
+ class Venue < BaseType; end
505
+ class ProximityAlertTriggered < BaseType; end
506
+ class WebAppData < BaseType; end
507
+ class PassportData < BaseType; end
508
+
509
+ class Invoice < BaseType; end
510
+ class SuccessfulPayment < BaseType; end
511
+ class ShippingAddress < BaseType; end
512
+ class OrderInfo < BaseType; end
513
+
514
+ class ShippingQuery < BaseType
515
+ def initialize(data)
516
+ super(data)
517
+ wrap('from', User)
518
+ wrap('shipping_address', ShippingAddress)
519
+ end
520
+ end
521
+
522
+ class PreCheckoutQuery < BaseType
523
+ def initialize(data)
524
+ super(data)
525
+ wrap('from', User)
526
+ wrap('shipping_address', ShippingAddress)
527
+ wrap('order_info', OrderInfo)
528
+ end
529
+ end
530
+
531
+ class PollOption < BaseType; end
532
+ class PollAnswer < BaseType; end
533
+
534
+ class Poll < BaseType
535
+ def initialize(data)
536
+ super(data)
537
+ wrap_array('options', PollOption)
538
+ wrap_array('explanation_entities', MessageEntity)
539
+ end
540
+ end
541
+
542
+ class ChatPermissions < BaseType; end
543
+ class ChatPhoto < BaseType; end
544
+ class ChatInviteLink < BaseType; end
545
+
546
+ # status-specific chat member objects. they inherit from ChatMember
547
+ class ChatMember < BaseType; end
548
+ class ChatMemberOwner < ChatMember; end
549
+ class ChatMemberAdministrator < ChatMember; end
550
+ class ChatMemberMember < ChatMember; end
551
+ class ChatMemberRestricted < ChatMember; end
552
+ class ChatMemberLeft < ChatMember; end
553
+ class ChatMemberBanned < ChatMember; end
554
+
555
+ class ChatAdministratorRights < BaseType; end
556
+
557
+ class ChatMemberUpdated < BaseType
558
+ def initialize(data)
559
+ super(data)
560
+ wrap('chat', Chat)
561
+ wrap('from', User)
562
+ wrap_member('old_chat_member')
563
+ wrap_member('new_chat_member')
564
+ wrap('invite_link', ChatInviteLink)
565
+ if @_raw_data['date'] && !@_raw_data['date'].is_a?(Time)
566
+ @_raw_data['date'] = Time.at(@_raw_data['date'])
567
+ end
568
+ end
569
+
570
+ private
571
+
572
+ def wrap_member(key)
573
+ return unless @_raw_data[key]
574
+ status = @_raw_data[key]['status']
575
+ klass = case status
576
+ when 'creator' then ChatMemberOwner
577
+ when 'administrator' then ChatMemberAdministrator
578
+ when 'member' then ChatMemberMember
579
+ when 'restricted' then ChatMemberRestricted
580
+ when 'left' then ChatMemberLeft
581
+ when 'kicked' then ChatMemberBanned
582
+ else ChatMember
583
+ end
584
+ @_raw_data[key] = klass.new(@_raw_data[key])
585
+ end
586
+ end
587
+
588
+ class ChatJoinRequest < BaseType
589
+ def initialize(data)
590
+ super(data)
591
+ wrap('chat', Chat)
592
+ wrap('from', User)
593
+ wrap('invite_link', ChatInviteLink)
594
+ if @_raw_data['date'] && !@_raw_data['date'].is_a?(Time)
595
+ @_raw_data['date'] = Time.at(@_raw_data['date'])
438
596
  end
439
597
  end
440
598
  end
599
+
600
+ class InlineQuery < BaseType
601
+ def initialize(data)
602
+ super(data)
603
+ wrap('from', User)
604
+ wrap('location', Location)
605
+ end
606
+ end
607
+
608
+ class ChosenInlineResult < BaseType
609
+ def initialize(data)
610
+ super(data)
611
+ wrap('from', User)
612
+ wrap('location', Location)
613
+ end
614
+ end
615
+
616
+ class InlineQueryResult < BaseType; end
617
+ class InlineQueryResultArticle < InlineQueryResult; end
618
+ class InlineQueryResultPhoto < InlineQueryResult; end
619
+ class InlineQueryResultGif < InlineQueryResult; end
620
+ class InlineQueryResultMpeg4Gif < InlineQueryResult; end
621
+ class InlineQueryResultVideo < InlineQueryResult; end
622
+ class InlineQueryResultAudio < InlineQueryResult; end
623
+ class InlineQueryResultVoice < InlineQueryResult; end
624
+ class InlineQueryResultDocument < InlineQueryResult; end
625
+ class InlineQueryResultLocation < InlineQueryResult; end
626
+ class InlineQueryResultVenue < InlineQueryResult; end
627
+ class InlineQueryResultContact < InlineQueryResult; end
628
+ class InlineQueryResultGame < InlineQueryResult; end
629
+ class InlineQueryResultSticker < InlineQueryResult; end
630
+ class InlineQueryResultCachedPhoto < InlineQueryResult; end
631
+ class InlineQueryResultCachedGif < InlineQueryResult; end
632
+ class InlineQueryResultCachedMpeg4Gif < InlineQueryResult; end
633
+ class InlineQueryResultCachedSticker < InlineQueryResult; end
634
+ class InlineQueryResultCachedDocument < InlineQueryResult; end
635
+ class InlineQueryResultCachedVideo < InlineQueryResult; end
636
+ class InlineQueryResultCachedAudio < InlineQueryResult; end
637
+ class InlineQueryResultCachedVoice < InlineQueryResult; end
638
+
639
+ class UserProfilePhotos < BaseType
640
+ def initialize(data)
641
+ super(data)
642
+ wrap_array('photos', PhotoSize)
643
+ end
644
+ end
645
+
646
+ class UserProfileAudios < BaseType
647
+ def initialize(data)
648
+ super(data)
649
+ wrap_array('audios', Audio)
650
+ end
651
+ end
652
+
653
+ # generic utility objects returned by the API
654
+ class File < BaseType; end
655
+ class ResponseParameters < BaseType; end
656
+ class MaskPosition < BaseType; end
657
+ class StickerSet < BaseType; end
658
+
659
+ # new message event payloads (each a simple wrapper)
660
+ class MessageAutoDeleteTimerChanged < BaseType; end
661
+ class ForumTopicCreated < BaseType; end
662
+ class ForumTopicEdited < BaseType; end
663
+ class ForumTopicClosed < BaseType; end
664
+ class ForumTopicReopened < BaseType; end
665
+ class GeneralForumTopicHidden < BaseType; end
666
+ class GeneralForumTopicUnhidden < BaseType; end
667
+ class WriteAccessAllowed < BaseType; end
668
+
669
+ class BotCommand < BaseType; end
670
+ class BotCommandScope < BaseType; end
671
+ class WebhookInfo < BaseType; end
672
+
673
+ class VideoChatScheduled < BaseType; end
674
+ class VideoChatStarted < BaseType; end
675
+ class VideoChatEnded < BaseType; end
676
+ class VideoChatParticipantsInvited < BaseType; end
677
+ class VideoChatLocation < BaseType; end
678
+
441
679
  end
442
680
  end
@@ -179,5 +179,5 @@ module Telegem
179
179
  end
180
180
  end
181
181
  end
182
- end
183
- end
182
+ end
183
+ end
@@ -1,125 +1,108 @@
1
- # lib/session/memory_store.rb - PRODUCTION READY
1
+ # lib/session/memory_store.rb
2
+ require 'json'
3
+ require 'time'
4
+ require 'fileutils'
5
+
2
6
  module Telegem
3
7
  module Session
4
8
  class MemoryStore
5
- def initialize
9
+ def initialize(
10
+ default_ttl: 300,
11
+ cleanup_interval: 300,
12
+ backup_path: nil,
13
+ backup_interval: 60
14
+ )
6
15
  @store = {}
7
16
  @ttls = {}
8
- @default_ttl = 300 # 5 minutes
9
- @cleanup_interval = 60 # Clean expired every minute
17
+ @default_ttl = default_ttl
18
+ @cleanup_interval = cleanup_interval
19
+ @backup_path = backup_path
20
+ @backup_interval = backup_interval
21
+
10
22
  @last_cleanup = Time.now
23
+ @last_backup = Time.now
24
+
25
+ restore! if @backup_path && File.exist?(@backup_path)
11
26
  end
12
27
 
13
- # Store with optional TTL
14
28
  def set(key, value, ttl: nil)
15
- auto_cleanup
16
- key_s = key.to_s
17
- @store[key_s] = value
18
- @ttls[key_s] = Time.now + (ttl || @default_ttl)
19
- value
20
- end
29
+ auto_cleanup
30
+ key_s = key.to_s
21
31
 
22
- # Get value if not expired
23
- def get(key)
24
- key_s = key.to_s
25
- return nil unless @store.key?(key_s)
26
-
27
- # Auto-clean if expired
28
- if expired?(key_s)
29
- delete(key_s)
30
- return nil
31
- end
32
-
33
- @store[key_s]
34
- end
32
+ @store[key_s] = value
33
+ @ttls[key_s] = Time.now + (ttl || @default_ttl)
35
34
 
36
- # Check if key exists and not expired
37
- def exist?(key)
38
- key_s = key.to_s
39
- return false unless @store.key?(key_s)
40
- !expired?(key_s)
41
- end
35
+ auto_backup
36
+ value
37
+ end
42
38
 
43
- # Delete key
44
- def delete(key)
45
- key_s = key.to_s
46
- @store.delete(key_s)
47
- @ttls.delete(key_s)
48
- true
49
- end
39
+ def get(key)
40
+ key_s = key.to_s
41
+ return nil unless @store.key?(key_s)
50
42
 
51
- # Increment counter (for rate limiting)
52
- def increment(key, amount = 1, ttl: nil)
53
- key_s = key.to_s
54
- current = get(key_s) || 0
55
- new_value = current + amount
56
- set(key_s, new_value, ttl: ttl)
57
- new_value
43
+ if expired?(key_s)
44
+ delete(key_s)
45
+ return nil
58
46
  end
59
47
 
60
- # Decrement counter
61
- def decrement(key, amount = 1)
62
- increment(key, -amount)
48
+ @store[key_s]
63
49
  end
64
50
 
65
- # Clear expired entries (auto-called)
66
- def cleanup
67
- now = Time.now
68
- @ttls.each do |key, expires|
69
- if now > expires
70
- @store.delete(key)
71
- @ttls.delete(key)
72
- end
73
- end
74
- @last_cleanup = now
75
- end
76
-
77
- # Clear everything
78
- def clear
79
- @store.clear
80
- @ttls.clear
81
- @last_cleanup = Time.now
82
- end
83
-
84
- # Get all keys (non-expired)
85
- def keys
86
- auto_cleanup
87
- @store.keys.select { |k| !expired?(k) }
88
- end
51
+ def delete(key)
52
+ key_s = key.to_s
53
+ @store.delete(key_s)
54
+ @ttls.delete(key_s)
55
+ true
56
+ end
89
57
 
90
- # Get size (non-expired entries)
91
- def size
92
- keys.size
58
+ def increment(key, amount = 1, ttl: nil)
59
+ current = get(key) || 0
60
+ # Ensure we are working with numbers
61
+ val = current.to_i rescue 0
62
+ new_val = val + amount
63
+ set(key, new_val, ttl: ttl)
64
+ new_val
93
65
  end
94
66
 
95
- def empty?
96
- size == 0
67
+ # --- Persistence Logic (The "Telecr" Way) ---
68
+
69
+ def backup!
70
+ return unless @backup_path
71
+
72
+ # 1. Prepare data
73
+ data = {
74
+ "store" => @store,
75
+ "ttls" => @ttls.transform_values(&:to_i), # Save as Unix timestamp
76
+ "timestamp" => Time.now.to_i
77
+ }
78
+
79
+ # 2. Ensure directory exists
80
+ FileUtils.mkdir_p(File.dirname(@backup_path))
81
+
82
+ # 3. ATOMIC WRITE: Write to temp, then rename
83
+ temp_path = "#{@backup_path}.tmp"
84
+ File.write(temp_path, JSON.generate(data))
85
+ File.rename(temp_path, @backup_path)
86
+
87
+ @last_backup = Time.now
97
88
  end
98
89
 
99
- # Get TTL remaining in seconds
100
- def ttl(key)
101
- key_s = key.to_s
102
- return -1 unless @ttls[key_s]
103
-
104
- remaining = @ttls[key_s] - Time.now
105
- remaining > 0 ? remaining.ceil : -1
106
- end
90
+ def restore!
91
+ return unless @backup_path && File.exist?(@backup_path)
107
92
 
108
- # Set TTL for existing key
109
- def expire(key, ttl)
110
- key_s = key.to_s
111
- return false unless @store.key?(key_s)
93
+ begin
94
+ raw = JSON.parse(File.read(@backup_path))
112
95
 
113
- @ttls[key_s] = Time.now + ttl
114
- true
115
- end
96
+ @store.clear
97
+ @ttls.clear
116
98
 
117
- # Redis-like scan for pattern matching
118
- def scan(pattern = "*", count: 10)
119
- auto_cleanup
120
- regex = pattern_to_regex(pattern)
121
- matching_keys = @store.keys.select { |k| k.match?(regex) && !expired?(k) }
122
- matching_keys.first(count)
99
+ raw["store"].each { |k, v| @store[k] = v }
100
+ raw["ttls"].each do |k, v|
101
+ @ttls[k] = Time.at(v)
102
+ end
103
+ rescue => e
104
+ warn "Telegem: Failed to restore backup: #{e.message}"
105
+ end
123
106
  end
124
107
 
125
108
  private
@@ -129,15 +112,19 @@ module Telegem
129
112
  end
130
113
 
131
114
  def auto_cleanup
132
- if Time.now - @last_cleanup > @cleanup_interval
133
- cleanup
115
+ if (Time.now - @last_cleanup) > @cleanup_interval
116
+ now = Time.now
117
+ expired_keys = @ttls.select { |_, expires| now > expires }.keys
118
+ expired_keys.each { |k| delete(k) }
119
+ @last_cleanup = now
134
120
  end
135
121
  end
136
122
 
137
- def pattern_to_regex(pattern)
138
- regex_str = pattern.gsub('*', '.*').gsub('?', '.')
139
- Regexp.new("^#{regex_str}$")
123
+ def auto_backup
124
+ if @backup_path && (Time.now - @last_backup) > @backup_interval
125
+ backup!
126
+ end
140
127
  end
141
128
  end
142
129
  end
143
- end
130
+ end
data/lib/telegem.rb CHANGED
@@ -3,7 +3,7 @@ require 'logger'
3
3
  require 'json'
4
4
 
5
5
  module Telegem
6
- VERSION = "3.3.0".freeze
6
+ VERSION = "3.3.1".freeze
7
7
  end
8
8
 
9
9
  #
@@ -78,4 +78,4 @@ if ENV['TELEGEM_GLOBAL'] == 'true'
78
78
  def Telegem(token, **options)
79
79
  ::Telegem.new(token, **options)
80
80
  end
81
- end
81
+ end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegem
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.3.1
5
5
  platform: ruby
6
6
  authors:
7
- - sick_phantom
7
+ - zendrx
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-04-24 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: concurrent-ruby
@@ -129,10 +130,11 @@ email:
129
130
  - ynwghosted@icloud.com
130
131
  executables:
131
132
  - telegem-ssl
133
+ - telegem-init
132
134
  extensions: []
133
135
  extra_rdoc_files: []
134
136
  files:
135
- - CHANGELOG
137
+ - CHANGELOG.md
136
138
  - CODE_OF_CONDUCT.md
137
139
  - Contributing.md
138
140
  - Gemfile
@@ -143,6 +145,7 @@ files:
143
145
  - assets/.gitkeep
144
146
  - assets/logo.png
145
147
  - bin/.gitkeep
148
+ - bin/telegem-init
146
149
  - bin/telegem-ssl
147
150
  - docs/.gitkeep
148
151
  - docs/ctx.md
@@ -167,18 +170,18 @@ files:
167
170
  - lib/webhook/.gitkeep
168
171
  - lib/webhook/server.rb
169
172
  - public/.gitkeep
170
- homepage: https://gitlab.com/ruby-telegem/telegem
173
+ homepage: https://github.com/slick-lab/telegem
171
174
  licenses:
172
175
  - MIT
173
176
  metadata:
174
- homepage_uri: https://gitlab.com/ruby-telegem/telegem/-/blob/main/README.md
175
- source_code_uri: https://gitlab.com/ruby-telegem/telegem
176
- changelog_uri: https://gitlab.com/ruby-telegem/telegem/-/blob/main/CHANGELOG.md
177
- bug_tracker_uri: https://gitlab.com/ruby-telegem/telegem/-/issues
177
+ homepage_uri: https://github.com/slick-lab/telegem/-/blob/main/README.md
178
+ source_code_uri: https://github.com/slick-lab/telegem
179
+ changelog_uri: https://github.com/slick-lab/telegem/-/blob/main/CHANGELOG.md
180
+ bug_tracker_uri: https://github.com/slick-lab/telegem/-/issues
178
181
  documentation_uri: https://gitlab.com/ruby-telegem/telegem/-/tree/main/docs-src?ref_type=heads
179
182
  rubygems_mfa_required: 'false'
180
- post_install_message: "Thanks for installing Telegem 3.3.0!\n\n\U0001F4DA Documentation:
181
- https://gitlab.com/ruby-telegem/telegem\n\n\U0001F510 For SSL Webhooks:\nRun: telegem-ssl
183
+ post_install_message: "Thanks for installing Telegem 3.3.1!\n\n\U0001F4DA Documentation:
184
+ https://github.com/slick-lab/telegem\n\n\U0001F510 For SSL Webhooks:\nRun: telegem-ssl
182
185
  your-domain.com\nThis sets up Let's Encrypt certificates automatically.\n\n\U0001F916
183
186
  Happy bot building!\n"
184
187
  rdoc_options: []
@@ -195,7 +198,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
198
  - !ruby/object:Gem::Version
196
199
  version: '0'
197
200
  requirements: []
198
- rubygems_version: 4.0.6
201
+ rubygems_version: 3.5.22
202
+ signing_key:
199
203
  specification_version: 4
200
204
  summary: Modern, fast Telegram Bot Framework for Ruby
201
205
  test_files: []
data/CHANGELOG DELETED
@@ -1,95 +0,0 @@
1
- Telegem Changelog
2
-
3
- v3.1.1 (current)
4
-
5
- 🚀 New Features
6
-
7
- · FileExtractor Plugin: New plugin for extracting content from various file types (PDF, JSON, HTML, TXT)
8
- · Async File Download: Added download method to API client for downloading Telegram files
9
- · Context File Helpers: Added download_file, download_photo, download_document methods to Context
10
- · Extended File Support: Plugin supports PDF text extraction, JSON parsing, HTML/raw text processing
11
- · Async/Sync Dual Mode: All file operations available in both sync (download) and async (download!) modes
12
-
13
- v3.1.0
14
-
15
- · BREAKING: Rewrote polling system to prevent duplicate messages
16
- · Fixed thread deadlock in async polling loop
17
- · Added scene_middleware.rb for scene-based conversations
18
- · Improved MemoryStore with TTL and thread safety
19
- · Enhanced keyboard markup builder with web_app support
20
- · Added message reaction and chat boost update types
21
- · Fixed callback query handling for inline keyboards
22
-
23
- v3.0.0
24
-
25
- · BREAKING: Complete async rewrite with async gem
26
- · New HTTP client using HTTPX with proper async/await pattern
27
- · Added scene system for multi-step conversations
28
- · Middleware composer system for plugin architecture
29
- · Type system with dynamic accessors for Telegram objects
30
- · Session management with memory store
31
- · Rate limiting middleware
32
- · File upload support via multipart forms
33
-
34
- v2.0.0
35
-
36
- · BREAKING: Ruby 3.0+ requirement
37
- · Added webhook support with Rack middleware
38
- · Inline query and callback query handlers
39
- · Location, contact, and poll answer handlers
40
- · Keyboard markup helpers (Telegem::Markup)
41
- · Improved error handling with custom error classes
42
- · Logging integration with configurable loggers
43
-
44
- v1.5.0
45
-
46
- · Added command argument parsing (ctx.command_args)
47
- · Message entity parsing (mentions, hashtags, bot commands)
48
- · Chat member update handlers
49
- · Pre-checkout and shipping query support
50
- · File download helper methods
51
- · Context helper methods for common API calls
52
-
53
- v1.0.0
54
-
55
- · Stable API release
56
- · Message handlers with text pattern matching
57
- · Command handlers with regex support
58
- · Basic context object with chat/message accessors
59
- · Simple API client with error handling
60
- · Polling and webhook modes
61
- · Configuration options for timeout and limits
62
-
63
- v0.5.0
64
-
65
- · Added callback query support
66
- · Inline keyboard builder
67
- · Message editing and deletion helpers
68
- · Media sending methods (photo, document, audio, video)
69
- · Chat action methods (typing, upload indicators)
70
-
71
- v0.3.0
72
-
73
- · Middleware system with bot.use
74
- · Session management foundation
75
- · Basic rate limiting
76
- · Command filtering by chat type
77
- · Improved logging with debug levels
78
-
79
- v0.2.0
80
-
81
- · Basic polling implementation
82
- · Message type detection (text, photo, document)
83
- · Command parsing with arguments
84
- · Simple reply methods
85
- · Error handling for API calls
86
-
87
- v0.1.0 (Initial Release)
88
-
89
- · Basic Telegram Bot API wrapper
90
- · Send/receive messages
91
- · Simple command handling
92
- · Minimal dependencies (just httparty)
93
- · Support for basic message types
94
-
95
- ---