telegem 3.2.4 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64cc027dc87d2e24f181a24705a85243f95c7f498a44f70fcb175d8a9b2dd597
4
- data.tar.gz: 37310a9a9c973f31f3dfd5b93e8347843a50c8218475b90a7f055df116c8353e
3
+ metadata.gz: 156ed056e2f5201d26aaa636b1bd2fdace0499dedca7aa52cff1692546480733
4
+ data.tar.gz: b0fef7cc585db195c4eeef5c0d656a659b1d12f8e9ea25506745c9318bc5dca8
5
5
  SHA512:
6
- metadata.gz: 9cd4708d09629445cf1398677521270c22991eee81b45204d2384570ba14876c937888cf86fc9ba8bfdad78138a48ea9bee6fed6e019a12ae89b29433f91e881
7
- data.tar.gz: ee85cb0026f096291a1700d825e68cf21f08385e1c7eaf5396763e00fbe4edecfb574b4ed1017293e4e41a06d60b98be5963893b6b0ba2e53f8816396ae33dfc
6
+ metadata.gz: a99c675b08fec66843ecc4d41880a74043a0498a486db404049e63eeb523ab746f186c482ce828550e010fa8f4f4ebd1102507e73532fb681c912c48c144417e
7
+ data.tar.gz: 0dc7360428b2d1fcfe383941f54c0b44f75af45c35eb7cb9cfbbfee190445c095bd5fac6007cd2ec885736ec8eea38ca9e286030b9b4486cd97ac4bef353d96c
data/Readme.md CHANGED
@@ -108,7 +108,7 @@ Perfect For:
108
108
 
109
109
  ---
110
110
 
111
- [📚 Documentation](https://rubydoc.info/gems/telegem/3.2.2/index)
111
+ [📚 Documentation](https://rubydoc.info/gems/telegem/3.2.3/index)
112
112
 
113
113
 
114
114
 
data/bin/telegem-ssl CHANGED
@@ -2,10 +2,17 @@
2
2
  # bin/telegem-ssl
3
3
 
4
4
  require 'fileutils'
5
+ require 'yaml'
5
6
 
6
7
  puts "🔐 Telegem SSL Setup"
7
8
  puts "=" * 10
8
9
 
10
+ if File.exist?('.telegem-ssl')
11
+ puts "🚫 SSL config already exists at .telegem-ssl"
12
+ print "Overwrite? (y/n): "
13
+ exit unless gets.chomp.downcase == 'y'
14
+ end
15
+
9
16
  # Get domain
10
17
  domain = ARGV[0]
11
18
  unless domain
data/lib/api/client.rb CHANGED
@@ -18,26 +18,19 @@ module Telegem
18
18
  end
19
19
 
20
20
  def call(method, params = {})
21
- Async do
22
21
  make_request(method, params)
23
- end.wait
24
22
  end
25
23
 
26
24
  def call!(method, params = {}, &callback)
27
25
  return unless callback
28
-
29
- Async do
30
26
  begin
31
27
  result = make_request(method, params)
32
28
  callback.call(result, nil)
33
29
  rescue => error
34
30
  callback.call(nil, error)
35
31
  end
36
- end
37
32
  end
38
-
39
33
  def upload(method, params)
40
- Async do
41
34
  url = "/bot#{@token}/#{method}"
42
35
 
43
36
  body = Async::HTTP::Body::Multipart.new
@@ -52,11 +45,10 @@ module Telegem
52
45
 
53
46
  response = @client.post(url, {}, body)
54
47
  handle_response(response)
55
- end.wait
48
+
56
49
  end
57
50
 
58
51
  def download(file_id, destination_path = nil)
59
- Async do
60
52
  file_info = call('getFile', file_id: file_id)
61
53
  return nil unless file_info && file_info['file_path']
62
54
 
@@ -76,7 +68,6 @@ module Telegem
76
68
  else
77
69
  raise NetworkError.new("Download failed: HTTP #{response.status}")
78
70
  end
79
- end.wait
80
71
  end
81
72
 
82
73
  def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
data/lib/api/types.rb CHANGED
@@ -294,6 +294,42 @@ module Telegem
294
294
  u.is_a?(User) ? u : User.new(u)
295
295
  end
296
296
  end
297
+
298
+ wrap_media_objects
299
+ end
300
+
301
+ 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
+
310
+ # Photo array
311
+ if @_raw_data['photo'] && @_raw_data['photo'].is_a?(Array)
312
+ @_raw_data['photo'] = @_raw_data['photo'].map do |p|
313
+ p.is_a?(BaseType) ? p : BaseType.new(p)
314
+ end
315
+ end
316
+
317
+ # 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
+
322
+ # 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)
325
+ @_raw_data['reply_markup'] = BaseType.new(@_raw_data['reply_markup']) if @_raw_data['reply_markup'] && !@_raw_data['reply_markup'].is_a?(BaseType)
326
+
327
+ # Chat photo array
328
+ if @_raw_data['new_chat_photo'] && @_raw_data['new_chat_photo'].is_a?(Array)
329
+ @_raw_data['new_chat_photo'] = @_raw_data['new_chat_photo'].map do |p|
330
+ p.is_a?(BaseType) ? p : BaseType.new(p)
331
+ end
332
+ end
297
333
  end
298
334
  end
299
335
 
data/lib/core/bot.rb CHANGED
@@ -1,5 +1,6 @@
1
+ #frozen_string_literal: true
1
2
  require 'json'
2
-
3
+ require 'async'
3
4
  module Telegem
4
5
  module Core
5
6
  class Bot
@@ -135,21 +136,29 @@ module Telegem
135
136
  end
136
137
 
137
138
  def set_my_profile_photo(photo, **options)
138
- @api.call('setMyProfilePhoto', { photo: photo }.merge(options))
139
- end
139
+ @api.call('setMyProfilePhoto', {
140
+ photo: photo
141
+ }.merge(options))
142
+ end
140
143
 
141
- def remove_my_profile_photo
142
- @api.call('removeMyProfilePhoto', {})
143
- end
144
+ def remove_my_profile_photo
145
+ @api.call('removeMyProfilePhoto', {})
146
+ end
144
147
 
145
- def get_user_profile_audios(user_id, **options)
146
- result = @api.call('getUserProfileAudios', { user_id: user_id }.merge(options))
147
- result ? Types::UserProfileAudios.new(result) : nil
148
- end
148
+ def get_user_profile_audios(user_id, **options)
149
+ result = @api.call('getUserProfileAudios', {
150
+ user_id: user_id
151
+ }.merge(options))
152
+ return nil unless result && result['audios']
153
+ Types::UserProfileAudios.new(result)
154
+ end
149
155
 
150
- def create_forum_topic(chat_id, name, **options)
151
- @api.call('createForumTopic', { chat_id: chat_id, name: name }.merge(options))
152
- end
156
+ def create_forum_topic(chat_id, name, **options)
157
+ @api.call('createForumTopic', {
158
+ chat_id: chat_id,
159
+ name: name
160
+ }.merge(options))
161
+ end
153
162
  def location(&block)
154
163
  on(:message, location: true) do |ctx|
155
164
  block.call(ctx)
@@ -230,12 +239,9 @@ module Telegem
230
239
 
231
240
  if updates && updates.any?
232
241
  updates.each do |update_data|
233
- Async do
234
242
  update = Types::Update.new(update_data)
235
243
  process_update(update)
236
244
  end
237
- end
238
-
239
245
  @offset = updates.last['update_id'] + 1
240
246
  @logger.debug("Updated offset to: #{@offset}")
241
247
  end
@@ -385,4 +391,4 @@ module Telegem
385
391
  end
386
392
  end
387
393
  end
388
- end
394
+ end
data/lib/core/context.rb CHANGED
@@ -71,7 +71,7 @@ module Telegem
71
71
  end
72
72
 
73
73
  def caption_entities
74
- message&caption_entities || []
74
+ message&.caption_entities || []
75
75
  end
76
76
 
77
77
  def caption
@@ -401,9 +401,6 @@ module Telegem
401
401
  def uploading_document(**options)
402
402
  send_chat_action('upload_document', **options)
403
403
  end
404
- def scene
405
- session[:telegem_scene]&.[](:id)
406
- end
407
404
  def ask(question, **options)
408
405
  scene_data = session[:telegem_scene]
409
406
  if scene_data
@@ -439,10 +436,15 @@ module Telegem
439
436
  scene&.next_step(self, step_name)
440
437
  end
441
438
  def with_typing(&block)
442
- typing_request = typing
443
-
439
+ thread = Thread.new do
440
+ while @typing_active
441
+ typing
442
+ sleep 5
443
+ end
444
+ end
444
445
  result = block.call
445
-
446
+ @typing_active = false
447
+ thread.join
446
448
  result
447
449
  end
448
450
 
@@ -64,36 +64,22 @@ module Telegem
64
64
 
65
65
  def increment_counters(ctx)
66
66
  now = Time.now.to_i
67
-
68
-
69
67
  if @options[:global]
70
68
  key = "global"
71
- cleanup_counter(:global, key, now)
72
69
  @counters[:global].increment(key, 1, ttl: @options[:global][:per])
73
70
  end
74
-
75
-
76
71
  if @options[:user] && ctx.from&.id
77
72
  key = "user:#{ctx.from.id}"
78
- cleanup_counter(:user, key, now)
79
73
  @counters[:user].increment(key, 1, ttl: @options[:user][:per])
80
74
  end
81
75
 
82
76
 
83
77
  if @options[:chat] && ctx.chat&.id
84
78
  key = "chat:#{ctx.chat.id}"
85
- cleanup_counter(:chat, key, now)
86
79
  @counters[:chat].increment(key, 1, ttl: @options[:chat][:per])
87
80
  end
88
81
  end
89
-
90
- def cleanup_counter(type, key, now)
91
- expires = @counters[type].get_ttl(key) || now
92
- @counters[type].delete(key) if now > expires
93
- end
94
-
95
82
  def rate_limit_response(ctx)
96
-
97
83
  ctx.reply("⏳ Please wait a moment before sending another request.") rescue nil
98
84
  nil
99
85
  end
data/lib/core/scene.rb CHANGED
@@ -124,7 +124,7 @@ module Telegem
124
124
  def process_response(ctx, scene_data)
125
125
 
126
126
  current_step_name = scene_data[:step]
127
- scene_data[:data][current_step_name] = ctx.message.text
127
+ scene_data[:data][current_step_name] = ctx.message
128
128
 
129
129
 
130
130
  scene_data[:waiting_for_response] = false
@@ -0,0 +1,104 @@
1
+ module Telegem
2
+ module Markup
3
+
4
+ module InlineButtons
5
+ def callback(text, data, style: nil, icon_custom_emoji_id: nil)
6
+ {
7
+ text: text,
8
+ callback_data: data,
9
+ style: style,
10
+ icon_custom_emoji_id: icon_custom_emoji_id
11
+ }.compact
12
+ end
13
+ def url(text, url, style: nil, icon_custom_emoji_id: nil)
14
+ {
15
+ text: text,
16
+ url: url,
17
+ style: style,
18
+ icon_custom_emoji_id: icon_custom_emoji_id
19
+ }.compact
20
+ end
21
+ def switch_inline(text, query: nil, style: nil, icon_custom_emoji_id: nil)
22
+ {
23
+ text: text,
24
+ switch_inline_query: query,
25
+ style: style,
26
+ icon_custom_emoji_id: icon_custom_emoji_id
27
+ }.compact
28
+ end
29
+ def switch_inline_current_chat(text, query: nil, style: nil, icon_custom_emoji_id: nil)
30
+ {
31
+ text: text,
32
+ switch_inline_query_current_chat: query,
33
+ style: style,
34
+ icon_custom_emoji_id: icon_custom_emoji_id
35
+ }.compact
36
+ end
37
+ def callback_game(text, game_short_name, style: nil, icon_custom_emoji_id: nil)
38
+ {
39
+ text: text,
40
+ callback_game: { short_name: game_short_name },
41
+ style: style,
42
+ icon_custom_emoji_id: icon_custom_emoji_id
43
+ }.compact
44
+ end
45
+ def pay(text, style: nil, icon_custom_emoji_id: nil)
46
+ {
47
+ text: text,
48
+ pay: true,
49
+ style: style,
50
+ icon_custom_emoji_id: icon_custom_emoji_id
51
+ }.compact
52
+ end
53
+ def web_app(text, url: nil, style: nil, icon_custom_emoji_id: nil)
54
+ {
55
+ text: text,
56
+ web_app: { url: url },
57
+ style: style,
58
+ icon_custom_emoji_id: icon_custom_emoji_id
59
+ }.compact
60
+ end
61
+ def login(text, url, style: nil, icon_custom_emoji_id: nil, **options)
62
+ login_url = { url: url}.merge(options)
63
+ {
64
+ text: text,
65
+ login_url: login_url,
66
+ style: style,
67
+ icon_custom_emoji_id: icon_custom_emoji_id
68
+ }.compact
69
+ end
70
+ end
71
+ class InlineBuilder
72
+ include InlineButtons
73
+ def initialize
74
+ @rows = []
75
+ end
76
+ def row(*buttons)
77
+ @rows << buttons
78
+ self
79
+ end
80
+ def build
81
+ InlineKeyboard.new(@rows)
82
+ end
83
+ end
84
+ class InlineKeyboard
85
+ attr_reader :rows
86
+ def initialize(rows)
87
+ @rows = rows
88
+ end
89
+ def to_h
90
+ {
91
+ inline_keyboard: @rows
92
+ }
93
+ end
94
+ def to_json(*args)
95
+ to_h.to_json(*args)
96
+ end
97
+ end
98
+ def self.inline(&block)
99
+ builder = InlineBuilder.new
100
+ builder.instance_eval(&block) if block_given?
101
+ builder.build
102
+ end
103
+ end
104
+ end
@@ -1,321 +1,100 @@
1
1
  module Telegem
2
2
  module Markup
3
- class Keyboard
4
- attr_reader :buttons, :options
5
3
 
6
- def initialize(buttons = [], **options)
7
- @buttons = buttons
4
+ module ReplyButtons
5
+ def text(content, style: nil, icon_custom_emoji_id: nil)
6
+ {
7
+ text: content,
8
+ icon_custom_emoji_id: icon_custom_emoji_id,
9
+ style: style
10
+ }.compact
11
+ end
12
+ def request_contact(text, style: nil, icon_custom_emoji_id: nil)
13
+ {
14
+ text: text,
15
+ style: style,
16
+ icon_custom_emoji_id: icon_custom_emoji_id,
17
+ request_contact: true
18
+ }.compact
19
+ end
20
+ def request_location(text, style: nil, icon_custom_emoji_id: nil)
21
+ {
22
+ text: text,
23
+ style: style,
24
+ icon_custom_emoji_id: icon_custom_emoji_id,
25
+ request_location: true
26
+ }.compact
27
+ end
28
+ def request_poll(text, poll_type: nil, style: nil, icon_custom_emoji_id: nil)
29
+ {
30
+ text: text,
31
+ style: style,
32
+ icon_custom_emoji_id: icon_custom_emoji_id,
33
+ request_poll: poll_type ? { type: poll_type } : {}
34
+ }.compact
35
+ end
36
+ def web_app(text, url: nil, style: nil, icon_custom_emoji_id: nil)
37
+ {
38
+ text: text,
39
+ url: url,
40
+ style: style,
41
+ icon_custom_emoji_id: icon_custom_emoji_id
42
+ }.compact
43
+ end
44
+ end
45
+ class ReplyBuilder
46
+ include ReplyButtons
47
+ def initialize
48
+ @rows = []
8
49
  @options = {
9
50
  resize_keyboard: true,
10
51
  one_time_keyboard: false,
11
52
  selective: false
12
- }.merge(options)
13
- end
14
-
15
- def self.[](*rows)
16
- new(rows)
17
- end
18
-
19
- def self.build(&block)
20
- builder = Builder.new
21
- builder.instance_eval(&block) if block_given?
22
- builder.keyboard
23
- end
24
-
25
- def row(*buttons)
26
- @buttons << buttons.flatten
27
- self
28
- end
29
-
30
- def button(text, style: nil, icon_custom_emoji_id: nil, **options)
31
- btn = {
32
- text: text
33
- }.merge(options)
34
- btn[:style] = style if style
35
- btn[:icon_custom_emoji_id] = icon_custom_emoji_id if icon_custom_emoji_id
36
- if @buttons.empty? || @buttons.last.is_a?(Array)
37
- @buttons << [btn]
38
- else
39
- @buttons.last << btn
40
- end
41
- self
42
- end
43
-
44
- def request_contact(text)
45
- button(text, request_contact: true)
46
- end
47
-
48
- def request_location(text)
49
- button(text, request_location: true)
50
- end
51
-
52
- def request_poll(text, type = nil)
53
- opts = type ? { request_poll: { type: type } } : { request_poll: {} }
54
- button(text, opts)
55
- end
56
-
57
- def resize(resize = true)
58
- @options[:resize_keyboard] = resize
59
- self
60
- end
61
-
62
- def one_time(one_time = true)
63
- @options[:one_time_keyboard] = one_time
64
- self
65
- end
66
-
67
- def selective(selective = true)
68
- @options[:selective] = selective
69
- self
70
- end
71
-
72
- def to_h
73
- {
74
- keyboard: @buttons.map { |row| row.is_a?(Array) ? row : [row] },
75
- **@options
76
53
  }
77
54
  end
78
-
79
- def to_json(*args)
80
- to_h.to_json(*args)
81
- end
82
-
83
- def self.remove(selective: false)
84
- {
85
- remove_keyboard: true,
86
- selective: selective
87
- }
88
- end
89
-
90
- def self.force_reply(selective: false, input_field_placeholder: nil)
91
- markup = {
92
- force_reply: true,
93
- selective: selective
94
- }
95
- markup[:input_field_placeholder] = input_field_placeholder if input_field_placeholder
96
- markup
97
- end
98
- end
99
-
100
- class InlineKeyboard
101
- attr_reader :buttons
102
-
103
- def initialize(buttons = [])
104
- @buttons = buttons
105
- end
106
-
107
- def self.[](*rows)
108
- new(rows)
109
- end
110
-
111
- def self.build(&block)
112
- builder = InlineBuilder.new
113
- builder.instance_eval(&block) if block_given?
114
- builder.keyboard
115
- end
116
-
117
55
  def row(*buttons)
118
- @buttons << buttons.flatten
56
+ @rows << buttons
119
57
  self
120
58
  end
121
-
122
- def button(text, style: nil, icon_custom_emoji_id: nil, **options)
123
- btn = {
124
- text: text
125
- }.merge(options)
126
- btn[:style] = style if style
127
- btn[:icon_custom_emoji_id] = icon_custom_emoji_id if icon_custom_emoji_id
128
- if @buttons.empty? || @buttons.last.is_a?(Array)
129
- @buttons << [btn]
130
- else
131
- @buttons.last << btn
132
- end
133
- self
134
- end
135
-
136
- def url(text, url)
137
- button(text, url: url)
138
- end
139
-
140
- def callback(text, data)
141
- button(text, callback_data: data)
142
- end
143
-
144
- def web_app(text, url)
145
- button(text, web_app: { url: url })
146
- end
147
-
148
- def login(text, url, **options)
149
- button(text, login_url: { url: url, **options })
150
- end
151
-
152
- def switch_inline(text, query = "")
153
- button(text, switch_inline_query: query)
154
- end
155
-
156
- def switch_inline_current(text, query = "")
157
- button(text, switch_inline_query_current_chat: query)
158
- end
159
-
160
- def pay(text)
161
- button(text, pay: true)
162
- end
163
-
164
- def to_h
165
- clean_rows = @buttons.compact.map do |row|
166
- row = Array(row).compact.select { |btn| is_a?(Hash) }
167
- row.empty? ? nil : row
168
- end.compact
169
- { inline_keyboard: clean_rows}
170
- end
171
-
172
- def to_json(*args)
173
- to_h.to_json(*args)
174
- end
175
- end
176
-
177
- class Builder
178
- attr_reader :keyboard
179
-
180
- def initialize
181
- @keyboard = Keyboard.new
182
- end
183
-
184
- def row(*buttons, &block)
185
- if block_given?
186
- sub_builder = Builder.new
187
- sub_builder.instance_eval(&block)
188
- @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
189
- elsif buttons.any?
190
- @keyboard.row(*buttons)
191
- else
192
- @keyboard.row
193
- end
59
+ def resize(value = true)
60
+ @options[:resize_keyboard] = value
194
61
  self
195
62
  end
196
-
197
- def button(text, **options)
198
- @keyboard.button(text, **options)
63
+ def one_time(value = true)
64
+ @options[:one_time_keyboard] = value
199
65
  self
200
66
  end
201
-
202
- def request_contact(text)
203
- @keyboard.request_contact(text)
67
+ def selective(value = true)
68
+ @options[:selective] = value
204
69
  self
205
70
  end
206
-
207
- def request_location(text)
208
- @keyboard.request_location(text)
71
+ def placeholder(text)
72
+ @options[:input_field_placeholder] = text
209
73
  self
210
74
  end
211
-
212
- def request_poll(text, type = nil)
213
- @keyboard.request_poll(text, type)
214
- self
215
- end
216
-
217
- def method_missing(name, *args, &block)
218
- if @keyboard && @keyboard.respond_to?(name)
219
- @keyboard.send(name, *args, &block)
220
- else
221
- super
222
- end
223
- end
224
-
225
- def respond_to_missing?(name, include_private = false)
226
- @keyboard && @keyboard.respond_to?(name) || super
227
- end
75
+ def build
76
+ ReplyKeyboard.new(@rows, @options)
228
77
  end
229
-
230
- class InlineBuilder
231
- attr_reader :keyboard
232
-
233
- def initialize
234
- @keyboard = InlineKeyboard.new
235
- end
236
-
237
- def row(*buttons, &block)
238
- if block_given?
239
- sub_builder = InlineBuilder.new
240
- sub_builder.instance_eval(&block)
241
- @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
242
- elsif buttons.any?
243
- @keyboard.row(*buttons)
244
- else
245
- @keyboard.row([])
78
+ end
79
+ class ReplyKeyboard
80
+ def initialize(rows, options = {})
81
+ @rows = rows
82
+ @options = options
246
83
  end
247
- self
248
- end
249
-
250
- def button(text, **options)
251
- @keyboard.button(text, **options)
252
- self
253
- end
254
-
255
- def url(text, url)
256
- @keyboard.url(text, url)
257
- self
258
- end
259
-
260
- def callback(text, data)
261
- @keyboard.callback(text, data)
262
- self
263
- end
264
-
265
- def web_app(text, url)
266
- @keyboard.web_app(text, url)
267
- self
268
- end
269
-
270
- def login(text, url, **options)
271
- @keyboard.login(text, url, **options)
272
- self
273
- end
274
-
275
- def switch_inline(text, query = "")
276
- @keyboard.switch_inline(text, query)
277
- self
278
- end
279
-
280
- def switch_inline_current(text, query = "")
281
- @keyboard.switch_inline_current(text, query)
282
- self
283
- end
284
-
285
- def pay(text)
286
- @keyboard.pay(text)
287
- self
288
- end
289
-
290
- def method_missing(name, *args, &block)
291
- if @keyboard && @keyboard.respond_to?(name)
292
- @keyboard.send(name, *args, &block)
293
- else
294
- super
84
+ def to_h
85
+ {
86
+ keyboard: @rows
87
+ }.merge(@options)
295
88
  end
296
- end
297
-
298
- def respond_to_missing?(name, include_private = false)
299
- @keyboard && @keyboard.respond_to?(name) || super
300
- end
301
- end
302
-
303
- class << self
304
- def keyboard(&block)
305
- Keyboard.build(&block)
306
- end
307
-
308
- def inline(&block)
309
- InlineKeyboard.build(&block)
310
- end
311
-
312
- def remove(**options)
313
- Keyboard.remove(**options)
314
- end
315
-
316
- def force_reply(**options)
317
- Keyboard.force_reply(**options)
318
- end
319
- end
320
- end
321
- end
89
+ def to_json(*args)
90
+ to_h.to_json(*args)
91
+ end
92
+ end
93
+ def self.keyboard(&block)
94
+ builder = ReplyBuilder.new
95
+ builder.instance_eval(&block) if block_given?
96
+ builder.build
97
+ end
98
+ end
99
+ end
100
+
@@ -85,28 +85,8 @@ module Telegem
85
85
  success: false,
86
86
  error: "Failed to extract PDF: #{e.message}"
87
87
  }
88
- rescue LoadError
89
- {
90
- success: false,
91
- error: "PDF extraction requires the 'pdf-reader' gem. Please add it to your Gemfile."
92
- }
93
- rescue PDF::Reader::MalformedPDFError => e
94
- {
95
- success: false,
96
- error: "Malformed PDF: #{e.message}"
97
- }
98
- rescue PDF::Reader::UnsupportedFeatureError => e
99
- {
100
- success: false,
101
- error: "Unsupported PDF feature: #{e.message}"
102
- }
103
- rescue PDF::Reader::EncryptedPDFError => e
104
- {
105
- success: false,
106
- error: "Encrypted PDF: #{e.message}"
107
- }
108
88
  ensure
109
- cleanup if @options[:auto_delete]
89
+ cleanup if @options[:auto_delete]
110
90
  end
111
91
  end
112
92
  def extract_json
@@ -196,8 +176,8 @@ module Telegem
196
176
  def cleanup
197
177
  @temp_file.unlink if @temp_file
198
178
  @temp_file = nil
199
- end
200
- end
201
- end
202
- end
203
- end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -5,7 +5,6 @@ module Telegem
5
5
  def initialize
6
6
  @store = {}
7
7
  @ttls = {}
8
- @mutex = Mutex.new
9
8
  @default_ttl = 300 # 5 minutes
10
9
  @cleanup_interval = 60 # Clean expired every minute
11
10
  @last_cleanup = Time.now
@@ -13,18 +12,15 @@ module Telegem
13
12
 
14
13
  # Store with optional TTL
15
14
  def set(key, value, ttl: nil)
16
- @mutex.synchronize do
17
15
  auto_cleanup
18
16
  key_s = key.to_s
19
17
  @store[key_s] = value
20
18
  @ttls[key_s] = Time.now + (ttl || @default_ttl)
21
19
  value
22
20
  end
23
- end
24
21
 
25
22
  # Get value if not expired
26
23
  def get(key)
27
- @mutex.synchronize do
28
24
  key_s = key.to_s
29
25
  return nil unless @store.key?(key_s)
30
26
 
@@ -36,37 +32,30 @@ module Telegem
36
32
 
37
33
  @store[key_s]
38
34
  end
39
- end
40
35
 
41
36
  # Check if key exists and not expired
42
37
  def exist?(key)
43
- @mutex.synchronize do
44
38
  key_s = key.to_s
45
39
  return false unless @store.key?(key_s)
46
40
  !expired?(key_s)
47
41
  end
48
- end
49
42
 
50
43
  # Delete key
51
44
  def delete(key)
52
- @mutex.synchronize do
53
45
  key_s = key.to_s
54
46
  @store.delete(key_s)
55
47
  @ttls.delete(key_s)
56
48
  true
57
49
  end
58
- end
59
50
 
60
51
  # Increment counter (for rate limiting)
61
52
  def increment(key, amount = 1, ttl: nil)
62
- @mutex.synchronize do
63
53
  key_s = key.to_s
64
54
  current = get(key_s) || 0
65
55
  new_value = current + amount
66
56
  set(key_s, new_value, ttl: ttl)
67
57
  new_value
68
58
  end
69
- end
70
59
 
71
60
  # Decrement counter
72
61
  def decrement(key, amount = 1)
@@ -75,7 +64,6 @@ module Telegem
75
64
 
76
65
  # Clear expired entries (auto-called)
77
66
  def cleanup
78
- @mutex.synchronize do
79
67
  now = Time.now
80
68
  @ttls.each do |key, expires|
81
69
  if now > expires
@@ -84,25 +72,20 @@ module Telegem
84
72
  end
85
73
  end
86
74
  @last_cleanup = now
87
- end
88
- end
75
+ end
89
76
 
90
77
  # Clear everything
91
78
  def clear
92
- @mutex.synchronize do
93
79
  @store.clear
94
80
  @ttls.clear
95
81
  @last_cleanup = Time.now
96
82
  end
97
- end
98
83
 
99
84
  # Get all keys (non-expired)
100
85
  def keys
101
- @mutex.synchronize do
102
86
  auto_cleanup
103
87
  @store.keys.select { |k| !expired?(k) }
104
88
  end
105
- end
106
89
 
107
90
  # Get size (non-expired entries)
108
91
  def size
@@ -115,34 +98,28 @@ module Telegem
115
98
 
116
99
  # Get TTL remaining in seconds
117
100
  def ttl(key)
118
- @mutex.synchronize do
119
101
  key_s = key.to_s
120
102
  return -1 unless @ttls[key_s]
121
103
 
122
104
  remaining = @ttls[key_s] - Time.now
123
105
  remaining > 0 ? remaining.ceil : -1
124
106
  end
125
- end
126
107
 
127
108
  # Set TTL for existing key
128
109
  def expire(key, ttl)
129
- @mutex.synchronize do
130
110
  key_s = key.to_s
131
111
  return false unless @store.key?(key_s)
132
112
 
133
113
  @ttls[key_s] = Time.now + ttl
134
114
  true
135
115
  end
136
- end
137
116
 
138
117
  # Redis-like scan for pattern matching
139
118
  def scan(pattern = "*", count: 10)
140
- @mutex.synchronize do
141
119
  auto_cleanup
142
120
  regex = pattern_to_regex(pattern)
143
121
  matching_keys = @store.keys.select { |k| k.match?(regex) && !expired?(k) }
144
122
  matching_keys.first(count)
145
- end
146
123
  end
147
124
 
148
125
  private
@@ -158,7 +135,6 @@ module Telegem
158
135
  end
159
136
 
160
137
  def pattern_to_regex(pattern)
161
- # Convert Redis-style pattern to Ruby regex
162
138
  regex_str = pattern.gsub('*', '.*').gsub('?', '.')
163
139
  Regexp.new("^#{regex_str}$")
164
140
  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.2.4".freeze
6
+ VERSION = "3.3.0".freeze
7
7
  end
8
8
 
9
9
  #
@@ -16,6 +16,7 @@ require_relative 'core/scene'
16
16
  require_relative 'session/middleware'
17
17
  require_relative 'session/memory_store'
18
18
  require_relative 'markup/keyboard'
19
+ require_relative 'markup/inline'
19
20
 
20
21
  require_relative 'plugins/file_extract'
21
22
  require_relative 'session/scene_middleware'
@@ -120,7 +120,7 @@ module Telegem
120
120
  begin
121
121
  body = request.body.read
122
122
  update_data = JSON.parse(body)
123
- Async { process_webhook_update(update_data) }
123
+ process_webhook_update(update_data)
124
124
  [200, {}, ["OK"]]
125
125
  rescue
126
126
  [500, {}, ["Internal Server Error"]]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegem
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.4
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sick_phantom
@@ -156,6 +156,7 @@ files:
156
156
  - lib/core/rate_limit.rb
157
157
  - lib/core/scene.rb
158
158
  - lib/markup/.gitkeep
159
+ - lib/markup/inline.rb
159
160
  - lib/markup/keyboard.rb
160
161
  - lib/plugins/.gitkeep
161
162
  - lib/plugins/file_extract.rb
@@ -176,7 +177,7 @@ metadata:
176
177
  bug_tracker_uri: https://gitlab.com/ruby-telegem/telegem/-/issues
177
178
  documentation_uri: https://gitlab.com/ruby-telegem/telegem/-/tree/main/docs-src?ref_type=heads
178
179
  rubygems_mfa_required: 'false'
179
- post_install_message: "Thanks for installing Telegem 3.2.4!\n\n\U0001F4DA Documentation:
180
+ post_install_message: "Thanks for installing Telegem 3.3.0!\n\n\U0001F4DA Documentation:
180
181
  https://gitlab.com/ruby-telegem/telegem\n\n\U0001F510 For SSL Webhooks:\nRun: telegem-ssl
181
182
  your-domain.com\nThis sets up Let's Encrypt certificates automatically.\n\n\U0001F916
182
183
  Happy bot building!\n"
@@ -194,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
195
  - !ruby/object:Gem::Version
195
196
  version: '0'
196
197
  requirements: []
197
- rubygems_version: 3.6.7
198
+ rubygems_version: 4.0.6
198
199
  specification_version: 4
199
200
  summary: Modern, fast Telegram Bot Framework for Ruby
200
201
  test_files: []